diff --git a/crates/cb-game-runtime/src/scenario.rs b/crates/cb-game-runtime/src/scenario.rs index 1a6aa08..ed47391 100644 --- a/crates/cb-game-runtime/src/scenario.rs +++ b/crates/cb-game-runtime/src/scenario.rs @@ -21,6 +21,16 @@ pub struct ScenarioFile { /// scenario, not the kernel. #[serde(default)] pub provisional: bool, + /// Who must rule on the U-item this scenario encodes. Required when + /// `provisional` is true: a provisional default with no owner can + /// shape the kernel indefinitely while looking handled + /// (CB-WP-0003 T08). + #[serde(default)] + pub provisional_owner: String, + /// ISO date the provisional default was raised, so its age is + /// reportable by `make coverage`. + #[serde(default)] + pub provisional_raised: String, pub seed: u64, pub setup: Setup, pub commands: Vec, diff --git a/scenarios/ground/gr-a04-bond-support.yaml b/scenarios/ground/gr-a04-bond-support.yaml index d70d957..f315220 100644 --- a/scenarios/ground/gr-a04-bond-support.yaml +++ b/scenarios/ground/gr-a04-bond-support.yaml @@ -5,6 +5,8 @@ description: > Bond, because GR-L02 requires the target's consent. covers: [GR-A04, GR-F04, GR-L02, GR-L05] provisional: true +provisional_owner: ground-game +provisional_raised: 2026-07-31 seed: 42 setup: players: 3 diff --git a/scenarios/ground/gr-d01-darvo-trigger.yaml b/scenarios/ground/gr-d01-darvo-trigger.yaml index 19ed4ab..fe99a86 100644 --- a/scenarios/ground/gr-d01-darvo-trigger.yaml +++ b/scenarios/ground/gr-d01-darvo-trigger.yaml @@ -5,6 +5,8 @@ description: > application (GR-F01 under the U2 default). covers: [GR-D01, GR-R08, GR-F01] provisional: true +provisional_owner: ground-game +provisional_raised: 2026-07-31 seed: 42 setup: players: 3 diff --git a/scenarios/ground/gr-d05-darvo-reverse.yaml b/scenarios/ground/gr-d05-darvo-reverse.yaml index 77efb18..f1e6d89 100644 --- a/scenarios/ground/gr-d05-darvo-reverse.yaml +++ b/scenarios/ground/gr-d05-darvo-reverse.yaml @@ -5,6 +5,8 @@ description: > owner takes −2 Stress and the sequence ends (GR-D05, GR-D07). covers: [GR-D05, GR-D07, GR-T01] provisional: true +provisional_owner: ground-game +provisional_raised: 2026-07-31 seed: 42 setup: players: 3 diff --git a/scenarios/ground/gr-e02-shared-ground.yaml b/scenarios/ground/gr-e02-shared-ground.yaml index 8182ef2..04e9aa7 100644 --- a/scenarios/ground/gr-e02-shared-ground.yaml +++ b/scenarios/ground/gr-e02-shared-ground.yaml @@ -5,6 +5,8 @@ description: > rating reduced by each Blame token and each Denied Problem. covers: [GR-R09, GR-E01, GR-E02, GR-P03] provisional: true +provisional_owner: ground-game +provisional_raised: 2026-07-31 seed: 42 setup: players: 3 diff --git a/scenarios/ground/gr-e04-coalitions.yaml b/scenarios/ground/gr-e04-coalitions.yaml index d9771ec..dd9f16d 100644 --- a/scenarios/ground/gr-e04-coalitions.yaml +++ b/scenarios/ground/gr-e04-coalitions.yaml @@ -5,6 +5,8 @@ description: > one. Personal scores subtract Blame (GR-E03, GR-T02). covers: [GR-E03, GR-E04, GR-O03] provisional: true +provisional_owner: ground-game +provisional_raised: 2026-07-31 seed: 42 setup: players: 3 diff --git a/specs/GroundRules.md b/specs/GroundRules.md index 78a1284..620b120 100644 --- a/specs/GroundRules.md +++ b/specs/GroundRules.md @@ -210,6 +210,15 @@ terms. ## Underdetermined in dataset 0.1 — PROVISIONAL defaults (flag to ground-game) +**Owner: ground-game. Raised: 2026-07-31.** Every U-item carries an owner +and a raise date so its age is visible; `make coverage` reports both and +warns past 30 days. CI **warns rather than breaks** — the ruling belongs +to ground-game and the kernel cannot make it for them — but every evidence +file that depends on one must list it (CB-WP-0003 T08). + +A ruling flips the *scenario*, not the kernel: the default is encoded in a +scenario tagged `provisional: true`, so a correction is a data change. + Formalization exposed points the dataset does not decide. Simulation uses the stated default; each is tagged in scenarios that depend on it and must be confirmed or corrected by ground-game. diff --git a/tools/rule-coverage.py b/tools/rule-coverage.py index 73ef5ae..109f9ed 100755 --- a/tools/rule-coverage.py +++ b/tools/rule-coverage.py @@ -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: diff --git a/workplans/CB-WP-0003-loop-hardening.md b/workplans/CB-WP-0003-loop-hardening.md index 33bdc57..fef7c1b 100644 --- a/workplans/CB-WP-0003-loop-hardening.md +++ b/workplans/CB-WP-0003-loop-hardening.md @@ -286,7 +286,7 @@ self-report of which one it was. ```task id: CB-WP-0003-T08 -status: todo +status: done priority: low state_hub_task_id: "c2ee91ee-e551-48a7-b1a9-477c34c0690c" ```