diff --git a/labs/opc-company/README.en.md b/labs/opc-company/README.en.md index 231e362..b5649b4 100644 --- a/labs/opc-company/README.en.md +++ b/labs/opc-company/README.en.md @@ -24,6 +24,18 @@ python3 scripts/opc.py failover \ --before fixtures/quota-failover/task.before.json \ --after fixtures/quota-failover/task.after.json # expected: same task, same owner, new worker, PASS + +# A blocked leaf execution path does not stop the canonical task +python3 scripts/opc.py continuation \ + --before fixtures/continuation/before.json \ + --after fixtures/continuation/after.bad-leaf-stop.json +# expected: rejected with leaf_blocker_promoted_global / premature_global_stop + +# An owner turn cannot drift to another scope without a semantic handoff +python3 scripts/opc.py continuation \ + --before fixtures/continuation/before.json \ + --after fixtures/continuation/after.bad-scope-drift.json +# expected: rejected with scope_drift_without_handoff ``` Public: protocols, schemas, synthetic fixtures, verifier. diff --git a/labs/opc-company/README.md b/labs/opc-company/README.md index 89e406d..3563595 100644 --- a/labs/opc-company/README.md +++ b/labs/opc-company/README.md @@ -28,6 +28,18 @@ python3 scripts/opc.py failover \ --before fixtures/quota-failover/task.before.json \ --after fixtures/quota-failover/task.after.json # 预期:同 task / 同 owner / 新 worker,PASS + +# 4) 叶子执行面被阻塞,不等于整个 canonical task 停止 +python3 scripts/opc.py continuation \ + --before fixtures/continuation/before.json \ + --after fixtures/continuation/after.bad-leaf-stop.json +# 预期:拒绝 leaf_blocker_promoted_global / premature_global_stop + +# 5) 无 semantic handoff 不允许 owner turn 漂到另一个 scope +python3 scripts/opc.py continuation \ + --before fixtures/continuation/before.json \ + --after fixtures/continuation/after.bad-scope-drift.json +# 预期:拒绝 scope_drift_without_handoff ``` ## 公开边界 diff --git a/labs/opc-company/fixtures/continuation/after.bad-leaf-stop.json b/labs/opc-company/fixtures/continuation/after.bad-leaf-stop.json new file mode 100644 index 0000000..85e9092 --- /dev/null +++ b/labs/opc-company/fixtures/continuation/after.bad-leaf-stop.json @@ -0,0 +1,16 @@ +{ + "schema_version": "opc.continuation.v1", + "canonical_task_id": "synthetic-content-task", + "scope_id": "opc-merge-milestone", + "work_continuing": false, + "global_status": "blocked", + "blocker_level": "leaf", + "leaf_blocker": { + "id": "x-native-schedule", + "reason": "long-post-schedule-disabled" + }, + "remaining_work": [ + "reuse_validation", + "incident_driven_opc_patch" + ] +} diff --git a/labs/opc-company/fixtures/continuation/after.bad-scope-drift.json b/labs/opc-company/fixtures/continuation/after.bad-scope-drift.json new file mode 100644 index 0000000..a622710 --- /dev/null +++ b/labs/opc-company/fixtures/continuation/after.bad-scope-drift.json @@ -0,0 +1,12 @@ +{ + "schema_version": "opc.continuation.v1", + "canonical_task_id": "synthetic-content-task", + "scope_id": "cross-business-scan", + "work_continuing": true, + "global_status": "active", + "blocker_level": "none", + "remaining_work": [ + "scan-cmd1", + "scan-cmd2" + ] +} diff --git a/labs/opc-company/fixtures/continuation/after.good-leaf-continue.json b/labs/opc-company/fixtures/continuation/after.good-leaf-continue.json new file mode 100644 index 0000000..dc7e747 --- /dev/null +++ b/labs/opc-company/fixtures/continuation/after.good-leaf-continue.json @@ -0,0 +1,16 @@ +{ + "schema_version": "opc.continuation.v1", + "canonical_task_id": "synthetic-content-task", + "scope_id": "opc-merge-milestone", + "work_continuing": true, + "global_status": "active", + "blocker_level": "leaf", + "leaf_blocker": { + "id": "x-native-schedule", + "reason": "long-post-schedule-disabled" + }, + "remaining_work": [ + "reuse_validation", + "incident_driven_opc_patch" + ] +} diff --git a/labs/opc-company/fixtures/continuation/before.json b/labs/opc-company/fixtures/continuation/before.json new file mode 100644 index 0000000..950e08b --- /dev/null +++ b/labs/opc-company/fixtures/continuation/before.json @@ -0,0 +1,12 @@ +{ + "schema_version": "opc.continuation.v1", + "canonical_task_id": "synthetic-content-task", + "scope_id": "opc-merge-milestone", + "work_continuing": true, + "global_status": "active", + "blocker_level": "none", + "remaining_work": [ + "x_release", + "reuse_validation" + ] +} diff --git a/labs/opc-company/scripts/opc.py b/labs/opc-company/scripts/opc.py index dfabc57..10a23e4 100644 --- a/labs/opc-company/scripts/opc.py +++ b/labs/opc-company/scripts/opc.py @@ -62,12 +62,33 @@ def cmd_failover(args): return 1 print(f"OPC_FAILOVER_PASS task={before.get('id')} from={before.get('primary_worker')} to={after.get('primary_worker')}"); return 0 +def cmd_continuation(args): + before=read_json(args.before); after=read_json(args.after); errors=[] + if before.get("canonical_task_id") != after.get("canonical_task_id"): + errors.append("canonical_task_changed") + scope_changed = before.get("scope_id") != after.get("scope_id") + handoff = after.get("scope_handoff") + if scope_changed: + if not isinstance(handoff, dict) or handoff.get("status") != "ADOPTED": + errors.append("scope_drift_without_handoff") + if after.get("blocker_level") == "leaf" and after.get("global_status") == "blocked": + errors.append("leaf_blocker_promoted_global") + remaining = after.get("remaining_work") or [] + if after.get("work_continuing") is False and remaining: + errors.append("premature_global_stop") + if errors: + for e in errors: print("OPC_CONTINUATION_REJECT", e) + return 1 + print(f"OPC_CONTINUATION_PASS task={after.get('canonical_task_id')} scope={after.get('scope_id')} work_continuing={str(bool(after.get('work_continuing'))).lower()}") + return 0 + def main(): ap=argparse.ArgumentParser(prog="opc"); sp=ap.add_subparsers(dest="cmd",required=True) p=sp.add_parser("lint"); p.add_argument("files",nargs="+"); p.set_defaults(fn=cmd_lint) p=sp.add_parser("status"); p.add_argument("company"); p.set_defaults(fn=cmd_status) p=sp.add_parser("verify"); p.add_argument("--task",required=True); p.add_argument("--receipt",required=True); p.set_defaults(fn=cmd_verify) p=sp.add_parser("failover"); p.add_argument("--before",required=True); p.add_argument("--after",required=True); p.set_defaults(fn=cmd_failover) + p=sp.add_parser("continuation"); p.add_argument("--before",required=True); p.add_argument("--after",required=True); p.set_defaults(fn=cmd_continuation) args=ap.parse_args(); raise SystemExit(args.fn(args)) if __name__=="__main__": main() diff --git a/labs/opc-company/tests/run.py b/labs/opc-company/tests/run.py index aaf146f..d812245 100644 --- a/labs/opc-company/tests/run.py +++ b/labs/opc-company/tests/run.py @@ -13,5 +13,8 @@ def run(args, expected): out=run(["verify","--task",str(ROOT/"fixtures/missing-ack/task.json"),"--receipt",str(ROOT/"fixtures/missing-ack/receipt.json")],1); assert "missing_semantic_ack" in out out=run(["verify","--task",str(ROOT/"examples/synthetic-company/tasks/product-release.json"),"--receipt",str(ROOT/"examples/synthetic-company/receipts/product-release.valid.json")],0); assert "OPC_VERIFY_PASS" in out out=run(["failover","--before",str(ROOT/"fixtures/quota-failover/task.before.json"),"--after",str(ROOT/"fixtures/quota-failover/task.after.json")],0); assert "OPC_FAILOVER_PASS" in out +out=run(["continuation","--before",str(ROOT/"fixtures/continuation/before.json"),"--after",str(ROOT/"fixtures/continuation/after.bad-leaf-stop.json")],1); assert "leaf_blocker_promoted_global" in out and "premature_global_stop" in out +out=run(["continuation","--before",str(ROOT/"fixtures/continuation/before.json"),"--after",str(ROOT/"fixtures/continuation/after.bad-scope-drift.json")],1); assert "scope_drift_without_handoff" in out +out=run(["continuation","--before",str(ROOT/"fixtures/continuation/before.json"),"--after",str(ROOT/"fixtures/continuation/after.good-leaf-continue.json")],0); assert "OPC_CONTINUATION_PASS" in out task_files=[str(p) for p in (ROOT/"examples/synthetic-company/tasks").glob("*.json")]; out=run(["lint",*task_files],0); assert "OPC_LINT_OK" in out print("OPC_TEST_OK")