P04-01: 同時発生の更新規約
This commit is contained in:
@@ -1,4 +1,39 @@
|
||||
# REPAIR40 implementation supersession; native evidence remains pending
|
||||
# REPAIR43 — native evidence and formal acceptance pending
|
||||
|
||||
The R42/R43 implementation batch now includes the public-restored G1/G2
|
||||
journal shadows, independent typed adapter oracle, exact-N idle OBJECTIVE
|
||||
breach, S5D_IN_FLIGHT event destruction, S5G_SHIFT_1080, S5H_PAUSED_MIDNIGHT,
|
||||
and S5E public same-minute advance_to after formal restore. See
|
||||
`../REPAIR41_STATUS.md` and `../REPAIR43_SOURCE_FACTS.json` for source bodies,
|
||||
lines, hashes and the spec EOF protocol. The closed-field/file-freeze blockers
|
||||
are superseded. The spec passes its existing checker protocol without edits.
|
||||
|
||||
Implementation is not native proof. Root must run the fresh nine-case S5
|
||||
Writer/Reader and adapters alongside fresh S1–S4 in the prescribed single
|
||||
batch. The new shift/paused/replay keys are emitted only after fresh typed raw
|
||||
derivation; paused_midnight is then removed from the adapter pending list.
|
||||
patience_release remains pending unless fresh S3 independently supplies it.
|
||||
Future-raw positives and mutations are Root-only. No historical run supplies
|
||||
these new keys. All 15+EV requirements and thresholds remain mandatory.
|
||||
|
||||
Pure results are in `../REPAIR43_PURE_RESULTS.json`; static results are in
|
||||
`../REPAIR41_STATIC_CHECKS.json`. r35/r36/R40 native remain FAILED, Q37 is only
|
||||
its own-version local PASS, and P04 credit remains 0. Registered validation
|
||||
precedes formal independent whole-unit review. No native/full run was performed
|
||||
by this worker; no completion claim or new counted unit is made.
|
||||
|
||||
## Historical REPAIR41 partial repair (superseded implementation status)
|
||||
|
||||
See `../REPAIR41_STATUS.md`: M serializes an additional `command_queue.accepted`
|
||||
metadata row for both guard failures, outside the explicitly enumerated G1/G2
|
||||
queue deltas. Its inclusion needs clarification before claiming exact equality.
|
||||
Only the mandatory-condition diagnostic and historical/fresh S4 wrapper
|
||||
separation are implemented and supersede their earlier repair-pending entries.
|
||||
S5D re-scope/G1/G2/breach, S5G/S5H, same-minute replay, adapter derivations and
|
||||
fresh-condition wiring remain pending. R40 native S5D remains FAILED; its prior
|
||||
source implementation below is not an accepted fixture. No P04 credit is earned.
|
||||
|
||||
## Historical REPAIR40 implementation supersession
|
||||
|
||||
The implementation-pending statements below are superseded only for S5A/S5B
|
||||
power, S5C command destruction, S5D automatic raid destruction, S5E paid midnight
|
||||
|
||||
@@ -1,3 +1,76 @@
|
||||
# REPAIR43 submission — Root verification required
|
||||
|
||||
The R41 closed journal-field restriction and R42 raw-only spec stop are
|
||||
superseded. The spec passes the existing one-final-LF checker condition.
|
||||
`../REPAIR41_STATUS.md` describes the complete R42/R43 implementation and
|
||||
`../REPAIR43_SOURCE_FACTS.json` contains full current-M source bodies/hashes.
|
||||
This is a QA submission, not admission, native PASS or formal completion.
|
||||
|
||||
The case table now includes S5A/B/C, S5D_DESTRUCTION_RAID (idle OBJECTIVE plus
|
||||
G1), S5D_IN_FLIGHT (event path plus G2), S5E/F, S5G_SHIFT_1080 and
|
||||
S5H_PAUSED_MIDNIGHT. The wrapper already derives CASES/LABELS/SLOTS from this
|
||||
table. Preserve the table-driven full roster. G1/G2 use public paused immediate
|
||||
submission with detached public-restored shadows; business state is identical
|
||||
and the whole typed journal is independently recomputed by the adapter.
|
||||
|
||||
Stage the entire QA hierarchy, including new `s5_guard_oracle.py`, with the
|
||||
selected exact Q. Worker static/pure outputs do not establish Q applicability.
|
||||
Run `s5_order_adapter_controls.py --pure-only` among affected pure controls.
|
||||
After fresh Reader raw exists, run its ordinary explicit
|
||||
`--raw-root/--writer-sha256/--reader-sha256/--scratch` mode as well; this executes
|
||||
the actual-data derivations and all typed order/guard mutations. Preserve the
|
||||
failed R40 native result and never reuse its missing Reader as a positive.
|
||||
|
||||
Authoritative corrected Root order:
|
||||
|
||||
1. Affected pure controls.
|
||||
2. Check-only S1–S5 plans.
|
||||
3. Capacity check under existing permissions, with no unapproved deletion.
|
||||
4. One heavy lease for fresh native S1–S5 including S5D/G/H, PID/cleanup checks
|
||||
and complete restore manifests.
|
||||
5. Reader and adapter on actual raw, including the fresh-raw mutation controls.
|
||||
6. Fresh collect.
|
||||
7. Full suite once, after source and controls are ready.
|
||||
8. Index.
|
||||
9. --verify-only.
|
||||
10. Registered validation.
|
||||
11. Formal independent whole-unit Opus review.
|
||||
12. Prepare / exact-C standing authorization / integration / final / DB join /
|
||||
ledger / push under the existing Root workflow.
|
||||
|
||||
No heavy tests while the worker is active. A second batch is only for defects
|
||||
revealed by observed native raw. The obsolete 500000-byte review-material cap
|
||||
does not apply; do not omit source/evidence or compress merely to meet it.
|
||||
|
||||
## Historical REPAIR41 direction (superseded by the order above)
|
||||
|
||||
The exact G1/G2 oracle has a source-contract conflict documented in
|
||||
`../REPAIR41_STATUS.md`. M also serializes one new `command_queue.accepted` row.
|
||||
Only the mandatory-condition diagnostic and historical/fresh S4 wrapper pin
|
||||
separation are repaired. S5D/G/H and adapter/fresh wiring remain unimplemented.
|
||||
Do not treat the partial pure-check results as worker success or native evidence.
|
||||
|
||||
After a complete submission, the recorded Root order remains:
|
||||
1. Affected pure controls.
|
||||
2. Check-only S1–S5 plan.
|
||||
3. Capacity check before the single heavy lease (about 3 GB free; OS swap is
|
||||
separate from evidence); only verified compression or approved cold-drive
|
||||
offload if needed, with no unapproved deletion.
|
||||
4. One heavy lease for fresh native S1–S5 including S5D/G/H, PID/cleanup checks,
|
||||
complete Git restore manifest and reuse of the ended Q.
|
||||
5. Reader and S5 order adapter on actual raw.
|
||||
6. Fresh collect.
|
||||
7. Full suite 184/329 once on final Q.
|
||||
8. Index.
|
||||
9. --verify-only.
|
||||
10. Independent Opus review of the whole unit.
|
||||
11. Registered validation.
|
||||
12. Normal prepare/final, exact-C approval, integration, Root DB join, ledger
|
||||
and main push.
|
||||
|
||||
This list preserves the latest recorded order over the earlier condensed list
|
||||
below. A second batch is only for defects revealed by observed native raw.
|
||||
|
||||
# REPAIR40 A–F source batch — Root verification required
|
||||
|
||||
This section supersedes earlier implementation-pending directions below only
|
||||
|
||||
+147
-9
@@ -4,11 +4,14 @@ const CASE_TABLE: Dictionary = {
|
||||
"S5A_PURE": {"expected_reserved_ids": 0, "labels": ["boundary", "held", "recovered", "duplicate_guard"], "prepare": "_prepare", "advance": "_advance", "stop": "_assert_stopped", "assert": "_assert_outcome"},
|
||||
"S5B_DEPENDENCY": {"expected_reserved_ids": 1, "labels": ["boundary", "held", "recovered", "duplicate_guard"], "prepare": "_prepare", "advance": "_advance", "stop": "_assert_stopped", "assert": "_assert_outcome"},
|
||||
"S5C_DESTRUCTION_COMMAND": {"expected_reserved_ids": 1, "labels": ["boundary", "held"], "prepare": "_prepare_order", "advance": "_advance_order", "stop": "_assert_order_stop", "assert": "_assert_order"},
|
||||
"S5D_DESTRUCTION_RAID": {"expected_reserved_ids": 1, "labels": ["boundary", "held"], "prepare": "_prepare_order", "advance": "_advance_order", "stop": "_assert_order_stop", "assert": "_assert_order"},
|
||||
"S5D_DESTRUCTION_RAID": {"expected_reserved_ids": 0, "labels": ["boundary", "held"], "prepare": "_prepare_idle_breach", "advance": "_advance_order", "stop": "_assert_idle_breach", "assert": "_assert_idle_order"},
|
||||
"S5D_IN_FLIGHT": {"expected_reserved_ids": 1, "labels": ["boundary", "held"], "prepare": "_prepare_order", "advance": "_advance_order", "stop": "_assert_in_flight", "assert": "_assert_in_flight_order"},
|
||||
"S5G_SHIFT_1080": {"expected_reserved_ids": 1, "labels": ["boundary", "held"], "prepare": "_prepare_order", "advance": "_advance_order", "stop": "_assert_order_stop", "assert": "_assert_order"},
|
||||
"S5H_PAUSED_MIDNIGHT": {"expected_reserved_ids": 1, "labels": ["boundary", "held", "recovered"], "prepare": "_prepare_order", "advance": "_advance_paused_midnight", "stop": "_assert_paused_midnight", "assert": "_assert_paused_midnight_order"},
|
||||
"S5E_MIDNIGHT": {"expected_reserved_ids": 1, "labels": ["boundary", "recalled"], "prepare": "_prepare_order", "advance": "_advance_order", "stop": "_assert_order_stop", "assert": "_assert_order"},
|
||||
"S5F_EXIT": {"expected_reserved_ids": 1, "labels": ["boundary", "held"], "prepare": "_prepare_order", "advance": "_advance_order", "stop": "_assert_order_stop", "assert": "_assert_order"}
|
||||
}
|
||||
const SEEDS: Dictionary = {"S5A_PURE": 420550, "S5B_DEPENDENCY": 420551, "S5C_DESTRUCTION_COMMAND": 420552, "S5D_DESTRUCTION_RAID": 420553, "S5E_MIDNIGHT": 424242, "S5F_EXIT": 420555}
|
||||
const SEEDS: Dictionary = {"S5A_PURE": 420550, "S5B_DEPENDENCY": 420551, "S5C_DESTRUCTION_COMMAND": 420552, "S5D_DESTRUCTION_RAID": 420553, "S5D_IN_FLIGHT": 420553, "S5E_MIDNIGHT": 424242, "S5F_EXIT": 420555, "S5G_SHIFT_1080": 420556, "S5H_PAUSED_MIDNIGHT": 424242}
|
||||
const GUEST: StringName = &"p04.s5.guest.a"
|
||||
const REQUEST: StringName = &"p04.s5.request.a"
|
||||
const QUEUED_GUEST: StringName = &"p04.s5.guest.b"
|
||||
@@ -36,6 +39,9 @@ var _points: Dictionary = {}
|
||||
var _branches: Dictionary = {}
|
||||
var _slots: Dictionary = {}
|
||||
var _replay_reference: Dictionary = {}
|
||||
var _guard_references: Dictionary = {}
|
||||
var _expected_failure: Dictionary = {}
|
||||
var _observed_phase: String = ""
|
||||
|
||||
func _run() -> void:
|
||||
_evidence_dir = OS.get_environment("THE_TREE_S5_EVIDENCE")
|
||||
@@ -69,6 +75,9 @@ func _fresh() -> void:
|
||||
_resource_command = {}
|
||||
_recovery = {}
|
||||
_replay_reference = {}
|
||||
_guard_references = {}
|
||||
_expected_failure = {}
|
||||
_observed_phase = ""
|
||||
_commands = []
|
||||
_results = []
|
||||
_receipts = []
|
||||
@@ -78,7 +87,10 @@ func _fresh() -> void:
|
||||
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_runtime.command_completed.connect(func(result: CommandResult) -> void:
|
||||
_results.append({&"minute": _clock.absolute_minute, &"result": result.to_dictionary()})
|
||||
_expect(result.is_success(), "actual public command completion " + str(result.command_id))
|
||||
if not _expected_failure.is_empty() and result.command_id == _expected_failure["command_id"]:
|
||||
_expect(var_to_bytes(result.to_dictionary()) == var_to_bytes(_expected_failure), "independently bound expected guard failure")
|
||||
else:
|
||||
_expect(result.is_success(), "actual public command completion " + str(result.command_id))
|
||||
)
|
||||
|
||||
func _cmd(kind: StringName, payload: Dictionary, at: int = -1) -> CommandResult:
|
||||
@@ -157,7 +169,8 @@ func _prepare() -> bool:
|
||||
|
||||
func _prepare_order() -> bool:
|
||||
_fresh()
|
||||
var midnight: bool = _case == "S5E_MIDNIGHT"
|
||||
var midnight: bool = _case in ["S5E_MIDNIGHT", "S5H_PAUSED_MIDNIGHT"]
|
||||
if _case == "S5G_SHIFT_1080": _boundary = 1080
|
||||
if midnight:
|
||||
_boundary = 1440
|
||||
var enabled: Dictionary = _runtime.enable_production_operations()
|
||||
@@ -180,6 +193,7 @@ func _prepare_order() -> bool:
|
||||
_cmd(&"register_staff", {&"staff_id": id, &"role_id": &"server", &"options": {&"state": StaffRuntime.State.OFF_DUTY, &"current_cell": Vector3i.ZERO}})
|
||||
_cmd(&"set_staff_work_assignment", {&"staff_id": id, &"template_id": &"NIGHT" if midnight else &"DAY", &"facility_id": _facility_id})
|
||||
var queue_at: int = _boundary - _duration - 1
|
||||
if _case == "S5D_IN_FLIGHT": queue_at += 1
|
||||
_expect(_price > 0 and _duration > 1 and queue_at > _clock.absolute_minute, "natural paid service before boundary")
|
||||
if queue_at <= _clock.absolute_minute or _duration <= 1: return false
|
||||
if _case == "S5D_DESTRUCTION_RAID":
|
||||
@@ -196,17 +210,28 @@ func _prepare_order() -> bool:
|
||||
if active.size() != 1: return false
|
||||
var service: ServiceInstance = active[0]
|
||||
_service_id = service.service_id
|
||||
_expect(service.started_at + _duration == _boundary, "natural N completion")
|
||||
_expect(service.started_at + _duration == _boundary + (1 if _case == "S5D_IN_FLIGHT" else 0), "natural completion from snapshot duration")
|
||||
_price = service.price_snapshot
|
||||
_duration = service.duration_minutes
|
||||
if _case == "S5D_DESTRUCTION_RAID":
|
||||
_clock.advance_minutes(_boundary - 5 - _clock.absolute_minute)
|
||||
_prepare_breach()
|
||||
_clock.advance_minutes(_boundary - 2 - _clock.absolute_minute)
|
||||
_queue(QUEUED_GUEST, QUEUED_REQUEST, _boundary + _duration + 1)
|
||||
if _case == "S5H_PAUSED_MIDNIGHT":
|
||||
_expect(_duration > 11, "pause begins before last pre-midnight MANA evaluation")
|
||||
_clock.advance_minutes(_boundary - 11 - _clock.absolute_minute)
|
||||
_reserve = _state.get_resource(&"mana")
|
||||
_cmd(&"spend_resources", {&"cost": {&"mana": _reserve}})
|
||||
_clock.advance_minutes(_boundary - 1 - _clock.absolute_minute)
|
||||
if _case == "S5F_EXIT": _queue(QUEUED_GUEST, QUEUED_REQUEST, _boundary + _duration + 1)
|
||||
_reserve = _state.get_resource(&"mana")
|
||||
if _case != "S5H_PAUSED_MIDNIGHT": _reserve = _state.get_resource(&"mana")
|
||||
_recovery = {&"staff_ids": staff_ids, &"resume_minute": _boundary}
|
||||
_expect(service.elapsed_minutes == _duration - 1, "duration minus one at N-1")
|
||||
if _case == "S5D_IN_FLIGHT":
|
||||
_guard_failure("G2", &"register_defense_asset", {&"raid_id": &"p04.s5.guard.raid", &"asset_id": _facility_id, &"kind": &"OBJECTIVE", &"max_integrity": 10000}, "A busy Facility cannot become a Defense asset.", {})
|
||||
_cmd(&"register_event_definition", {&"definition": {&"event_id": &"p04.s5.break", &"category": &"FLAVOR", &"priority": 100, &"foreground": false, &"transaction_policy": &"ROLLBACK_ALL", &"automatic_actions": [{&"action_id": &"break", &"type": &"SET_FACILITY_STATE", &"payload": {&"facility_id": _facility_id, &"conditions": {&"durability": 0, &"suspension_reason": &"DESTROYED"}}}]}})
|
||||
if _case not in ["S5D_IN_FLIGHT", "S5H_PAUSED_MIDNIGHT"]:
|
||||
_expect(service.elapsed_minutes == _duration - 1, "duration minus one at N-1")
|
||||
return _clock.absolute_minute == _boundary - 1
|
||||
|
||||
func _prepare_paid_reception() -> void:
|
||||
@@ -240,9 +265,112 @@ func _prepare_breach() -> void:
|
||||
_cmd(&"start_defense_raid", {&"raid_id": &"p04.s5.raid"})
|
||||
var due: Array[Dictionary] = _runtime.defense_service.get_due_rounds(_boundary)
|
||||
_expect(due.size() == 1 and due[0][&"earliest_minute"] == _boundary and due[0][&"raid_id"] == &"p04.s5.raid", "automatic breach due at exactly N; no substitute")
|
||||
_recovery[&"due_rounds"] = due.duplicate(true)
|
||||
|
||||
func _guard_failure(label: String, kind: StringName, payload: Dictionary, message: String, details: Dictionary) -> void:
|
||||
# Paused immediate public execution: no tick/business transition is permitted.
|
||||
_clock.set_paused(true)
|
||||
var before: Dictionary = _runtime.create_snapshot().duplicate(true)
|
||||
var expected: Dictionary = before.duplicate(true)
|
||||
var journal: Dictionary = expected[&"systems"][&"runtime"]
|
||||
_expect(journal[&"command_queue"]["pending"].is_empty() and journal[&"command_queue"]["in_flight"].is_empty(), "isolated guard command")
|
||||
_expect(not journal[&"operations"][&"enabled"], "guard has no population synchronization")
|
||||
var queue: CommandQueue = CommandQueue.new()
|
||||
var ids: StableIdGenerator = StableIdGenerator.new()
|
||||
var events: DomainEventStream = DomainEventStream.new()
|
||||
_expect(queue.restore_snapshot(journal[&"command_queue"]), "shadow queue public restore")
|
||||
_expect(ids.restore_snapshot(journal[&"id_generator"]), "shadow ID public restore")
|
||||
_expect(events.restore_snapshot(journal[&"domain_events"]), "shadow stream public restore")
|
||||
_serial += 1
|
||||
var key: StringName = StringName("p04.s5.%s.command.%d" % [_case, _serial])
|
||||
var command: SimulationCommand = SimulationCommand.new(journal[&"next_command_id"], _clock.absolute_minute, _clock.absolute_minute, 10, kind, &"player", &"", payload, key)
|
||||
var failure: CommandResult = CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, message, details)
|
||||
_expected_failure = failure.to_dictionary()
|
||||
_expect(queue.enqueue(command).is_success(), "shadow enqueue")
|
||||
var completed: Array[CommandResult] = queue.execute_ready(_clock.absolute_minute, func(_command: SimulationCommand) -> CommandResult: return failure.copy())
|
||||
_expect(completed.size() == 1, "one shadow failure")
|
||||
var entities: Array[StringName] = []
|
||||
var event: SimulationDomainEvent = SimulationDomainEvent.new(ids.next_id(&"domain_event"), &"COMMAND_FAILED", _clock.absolute_minute, &"simulation_runtime", entities, failure.to_dictionary(), command.command_id)
|
||||
_expect(events.publish(event), "exact M failed-result event")
|
||||
journal[&"next_command_id"] += 1
|
||||
journal[&"command_queue"] = queue.create_snapshot()
|
||||
journal[&"id_generator"] = ids.create_snapshot()
|
||||
journal[&"domain_events"] = events.create_snapshot()
|
||||
# EXPECTED is complete before submit, never read back from AFTER.
|
||||
var expected_bytes: PackedByteArray = var_to_bytes(expected)
|
||||
var actual: CommandResult = _runtime.submit_command(kind, payload, &"player", &"", 10, -1, key)
|
||||
var after: Dictionary = _runtime.create_snapshot()
|
||||
_expect(var_to_bytes(actual.to_dictionary()) == var_to_bytes(failure.to_dictionary()), "guard exact failed result")
|
||||
_expect(expected_bytes == var_to_bytes(after), "whole typed guard equality with no masks")
|
||||
var callbacks: int = 0
|
||||
for row: Dictionary in _results:
|
||||
if row[&"result"]["command_id"] == command.command_id: callbacks += 1
|
||||
_expect(callbacks == 1, "one failed command_completed callback")
|
||||
_guard_references[label] = _write(label, {&"before": before, &"expected": expected, &"after": after, &"command": command.to_dictionary(), &"result": actual.to_dictionary(), &"guard": label})
|
||||
_expected_failure = {}
|
||||
|
||||
func _prepare_idle_breach() -> bool:
|
||||
_fresh()
|
||||
var facility: FacilityInstance = _build_cafeteria(true)
|
||||
if facility == null: return false
|
||||
_facility_id = facility.instance_id
|
||||
_recovery = {&"staff_ids": [], &"resume_minute": _boundary}
|
||||
_clock.advance_minutes(_boundary - 5 - _clock.absolute_minute)
|
||||
_prepare_breach()
|
||||
_guard_failure("G1", &"create_service_request", {&"facility_id": _facility_id, &"request_id": REQUEST, &"guest_ids": [GUEST]}, "A Facility assigned to a non-terminal Defense Raid cannot accept service requests.", {&"raid_id": &"p04.s5.raid"})
|
||||
_clock.advance_minutes(_boundary - 1 - _clock.absolute_minute)
|
||||
return _clock.absolute_minute == _boundary - 1
|
||||
|
||||
func _assert_idle_breach(point: Dictionary) -> void:
|
||||
_expect(point[&"facility"][&"durability"] == 0 and point[&"facility"][&"state"] == "BROKEN" and point[&"facility"][&"suspension_reason"] == "DESTROYED", "idle OBJECTIVE automatically destroyed")
|
||||
_expect(point[&"active_services"] == 0 and point[&"service"].is_empty() and point[&"reservations"].is_empty(), "idle breach has no services or reservations")
|
||||
for request: Dictionary in point[&"requests"].values(): _expect(request.is_empty(), "idle breach no request/orphan")
|
||||
for entry: Dictionary in point[&"ledger"]: _expect(entry[&"category"] != "SERVICE_SALE", "idle breach no sale")
|
||||
|
||||
func _assert_idle_order(_before: Dictionary, point: Dictionary) -> void:
|
||||
_assert_idle_breach(point)
|
||||
|
||||
func _assert_in_flight(point: Dictionary) -> void:
|
||||
_expect(point[&"facility"][&"durability"] == 0 and point[&"facility"][&"state"] == "BROKEN", "event destroys active facility")
|
||||
_expect(point[&"service"][&"state"] == ("ACTIVE" if point[&"minute"] == _boundary else "FAILED"), "N active then N+1 failure")
|
||||
|
||||
func _assert_in_flight_order(before: Dictionary, point: Dictionary) -> void:
|
||||
_assert_in_flight(point)
|
||||
_expect(point[&"wallets"] == before[&"wallets"], "in-flight destruction zero charge")
|
||||
if point[&"minute"] > _boundary: _assert_reservation(before, point, REQUEST, "RELEASED", _boundary + 1)
|
||||
else: _assert_reservation(before, point, REQUEST, "ACTIVE", -1)
|
||||
|
||||
func _advance_paused_midnight(label: String) -> void:
|
||||
if label != "recovered":
|
||||
_clock.advance_one_minute()
|
||||
return
|
||||
_cmd(&"change_resources", {&"deltas": {&"mana": _reserve}})
|
||||
_recovery[&"resume_minute"] = -1
|
||||
while _clock.absolute_minute < _boundary + _duration + 20:
|
||||
_clock.advance_one_minute()
|
||||
if _runtime.facility_service.get_service(_service_id).state == ServiceInstance.State.COMPLETED:
|
||||
_recovery[&"resume_minute"] = _clock.absolute_minute
|
||||
break
|
||||
_expect(_recovery[&"resume_minute"] > _boundary, "bounded new-day completion")
|
||||
|
||||
func _assert_paused_midnight(point: Dictionary) -> void:
|
||||
_expect(point[&"service"][&"state"] == "PAUSED" and point[&"service"][&"interruption_reason"] == "RELATED_FACILITY_INACTIVE", "legitimate dependency pause across DAILY_CLOSE")
|
||||
_expect(point[&"operations_enabled"] and not point[&"actual_coordinator_absent"], "real DAILY_CLOSE coordinator")
|
||||
|
||||
func _assert_paused_midnight_order(before: Dictionary, point: Dictionary) -> void:
|
||||
if point[&"minute"] <= _boundary + 1:
|
||||
_assert_paused_midnight(point)
|
||||
_expect(point[&"service"][&"elapsed_minutes"] == before[&"service"][&"elapsed_minutes"], "paused elapsed unchanged")
|
||||
_expect(point[&"wallets"] == before[&"wallets"], "no paused payment")
|
||||
_assert_reservation(before, point, REQUEST, "ACTIVE", -1)
|
||||
else:
|
||||
_expect(point[&"service"][&"state"] == "COMPLETED", "new-day recovered completion")
|
||||
_expect(point[&"wallets"][String(GUEST)] == before[&"wallets"][String(GUEST)] - _price, "one recovered price snapshot debit")
|
||||
_assert_reservation(before, point, REQUEST, "CONSUMED", point[&"minute"])
|
||||
|
||||
func _advance_order(label: String) -> void:
|
||||
if label == "boundary":
|
||||
if _case == "S5D_IN_FLIGHT": _cmd(&"enqueue_event", {&"event_id": &"p04.s5.break", &"actor_ids": [&"player"]}, _boundary)
|
||||
if _case == "S5C_DESTRUCTION_COMMAND": _cmd(&"set_facility_conditions", {&"facility_id": _facility_id, &"conditions": {&"durability": 0}}, _boundary)
|
||||
if _case == "S5F_EXIT": _cmd(&"leave_facility_queue", {&"facility_id": _facility_id, &"request_id": QUEUED_REQUEST}, _boundary)
|
||||
if label == "recalled":
|
||||
@@ -251,6 +379,7 @@ func _advance_order(label: String) -> void:
|
||||
_expect(replay.is_success() and replay.is_replay, "same midnight is a replay")
|
||||
_expect(var_to_bytes(before) == var_to_bytes(_runtime.create_snapshot()), "same-minute replay changes no typed state")
|
||||
_recovery[&"midnight_replay"] = replay.to_dictionary()
|
||||
_replay_reference = _write("same-minute-replay", {&"before": before, &"after": _runtime.create_snapshot(), &"result": replay.to_dictionary(), &"minute": _boundary})
|
||||
else:
|
||||
_clock.advance_one_minute()
|
||||
|
||||
@@ -281,12 +410,19 @@ func _connect_observations() -> void:
|
||||
_runtime.economy_service.transaction_posted.connect(func(id: StringName, entries: Array) -> void:
|
||||
var rows: Array = []
|
||||
for entry: LedgerEntry in entries: rows.append(entry.to_dictionary())
|
||||
_receipts.append({&"id": id, &"minute": _clock.absolute_minute, &"entries": rows})
|
||||
var staff_at_receipt: Array = []
|
||||
for staff_id: StringName in _recovery.get(&"staff_ids", []):
|
||||
staff_at_receipt.append(_runtime.staff_service.get_staff(staff_id).create_snapshot())
|
||||
_receipts.append({&"id": id, &"minute": _clock.absolute_minute, &"phase": _observed_phase, &"entries": rows, &"staff_at_receipt": staff_at_receipt})
|
||||
)
|
||||
_clock.tick_phase_started.connect(func(phase: int, minute: int) -> void:
|
||||
_observed_phase = SimulationKernel.TickPhase.keys()[phase]
|
||||
var label: String = "phase-%d-%s" % [minute, SimulationKernel.TickPhase.keys()[phase]]
|
||||
var row: Dictionary = _capture()
|
||||
row[&"phase"] = SimulationKernel.TickPhase.keys()[phase]
|
||||
if _case == "S5G_SHIFT_1080" and minute == 1080:
|
||||
var historical: Dictionary = {SimulationKernel.TickPhase.FACILITY_COMPLETION: "first-18-00-before-facility", SimulationKernel.TickPhase.VITALS_AND_ENVIRONMENT: "first-18-00-after-facility", SimulationKernel.TickPhase.PUBLISH_SNAPSHOT: "first-18-00-after-staff"}
|
||||
row[&"order_label"] = historical.get(phase, "")
|
||||
_assert_phase(row)
|
||||
_phases.append(_write(label, row))
|
||||
)
|
||||
@@ -316,6 +452,7 @@ func _capture() -> Dictionary:
|
||||
var ledger: Array = []
|
||||
for entry: LedgerEntry in _runtime.economy_service.get_entries(): ledger.append(entry.to_dictionary())
|
||||
return {&"snapshot": snapshot, &"minute": _clock.absolute_minute,
|
||||
&"order_label": ({1079: "first-before-17-59", 1080: "first-18-00-tick-complete", 1081: "first-18-01-tick-complete"}.get(_clock.absolute_minute, "") if _case == "S5G_SHIFT_1080" else ""),
|
||||
&"inputs": {&"case": _case, &"seed": SEEDS[_case], &"serial": _serial, &"facility_id": _facility_id, &"service_id": _service_id,
|
||||
&"boundary": _boundary, &"price": _price, &"duration": _duration, &"patience": _patience, &"reserve": _reserve, &"recovery": _recovery.duplicate(true),
|
||||
&"kitchen_premise": "ActorHardening physical FLEX fixture; not natural hire/unlock"},
|
||||
@@ -397,7 +534,7 @@ func _writer_case() -> void:
|
||||
var guard: Dictionary = _capture()
|
||||
_assert_outcome(before, guard)
|
||||
_points["duplicate_guard"] = _write("duplicate_guard", guard)
|
||||
_branches[_case] = _write("case", {&"resource_replay": _replay_reference, &"points": _points.duplicate(true), &"phases": _phases.duplicate(true),
|
||||
_branches[_case] = _write("case", {&"resource_replay": _replay_reference, &"guards": _guard_references, &"points": _points.duplicate(true), &"phases": _phases.duplicate(true),
|
||||
&"commands": _write("commands", {&"submitted": _commands, &"completed_including_fixture": _results})})
|
||||
|
||||
func _assert_before(point: Dictionary) -> void:
|
||||
@@ -554,6 +691,7 @@ func _assert_expected_phase_roster(references: Array) -> void:
|
||||
var expected_pairs: Array = []
|
||||
var end: int = int(_recovery[&"resume_minute"]) + 1
|
||||
if _case == "S5E_MIDNIGHT": end = _boundary
|
||||
if _case == "S5H_PAUSED_MIDNIGHT": end = int(_recovery[&"resume_minute"])
|
||||
for minute: int in range(_boundary, end):
|
||||
for phase: int in range(SimulationKernel.TickPhase.size()):
|
||||
expected_pairs.append([minute if phase == SimulationKernel.TickPhase.APPLY_COMMANDS else minute + 1, SimulationKernel.TickPhase.keys()[phase]])
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Engine-free mirror of M journal serialization; EXPECTED never accepts AFTER.
|
||||
|
||||
The native probe uses detached public restores. This independent derivation
|
||||
binds that recorded EXPECTED to BEFORE and the fixed guard command, so copying
|
||||
AFTER into EXPECTED cannot turn a mutation into evidence.
|
||||
"""
|
||||
import struct
|
||||
|
||||
MESSAGES = {
|
||||
'G1': 'A Facility assigned to a non-terminal Defense Raid cannot accept service requests.',
|
||||
'G2': 'A busy Facility cannot become a Defense asset.',
|
||||
}
|
||||
|
||||
|
||||
def atom(kind, value):
|
||||
if kind in (4, 21):
|
||||
data = value.encode()
|
||||
return struct.pack('<II', kind, len(data)) + data + b'\0' * (-len(data) % 4)
|
||||
if kind == 2 and not -(2**31) <= value < 2**31:
|
||||
return struct.pack('<Iq', kind | 65536, value)
|
||||
return struct.pack('<Ii', kind, value)
|
||||
|
||||
|
||||
def dictionary(pairs):
|
||||
return struct.pack('<II', 27, len(pairs)) + b''.join(k + v for k, v in pairs)
|
||||
|
||||
|
||||
def strings_dict(pairs):
|
||||
return dictionary([(atom(4, k), v) for k, v in pairs])
|
||||
|
||||
|
||||
def array(rows, element=None):
|
||||
header = struct.pack('<II', 28, len(rows)) if element is None else struct.pack('<III', 65564, element, len(rows))
|
||||
return header + b''.join(rows)
|
||||
|
||||
|
||||
def update(node, changes):
|
||||
"""Retain all untouched typed keys, values and order, with no field masks."""
|
||||
if node.kind != 27 or set(changes) - {k.value for k, _ in node.value}:
|
||||
raise ValueError('journal dictionary keys')
|
||||
return dictionary([(k.raw, changes.get(k.value, v.raw)) for k, v in node.value])
|
||||
|
||||
|
||||
def expected(before, command, guard, parse, require):
|
||||
require(guard in MESSAGES, 'fixed guard')
|
||||
rt = before.at('systems').at('runtime')
|
||||
q = rt.at('command_queue')
|
||||
require(q.at('version').kind == 2 and q.at('version').value == 2, 'M queue version')
|
||||
require(rt.at('next_command_id').kind == 2, 'typed next_command_id')
|
||||
for name in ('pending', 'in_flight', 'processed', 'accepted'):
|
||||
require(q.at(name).raw[:8] == struct.pack('<II', 65564, 27), 'M typed queue array: ' + name)
|
||||
prior_ids = {}
|
||||
for name in ('accepted', 'processed'):
|
||||
members = q.at(name).value
|
||||
require(all(r.at('command_id').kind == 2 for r in members), 'typed prior command IDs')
|
||||
numbers = [r.at('command_id').value for r in members]
|
||||
require(numbers == sorted(set(numbers)) and all(i > 0 for i in numbers), 'sorted unique ' + name)
|
||||
prior_ids[name] = numbers
|
||||
require(prior_ids['accepted'] == prior_ids['processed'], 'complete isolated prior journal')
|
||||
mapping = q.at('idempotency')
|
||||
require(all(k.kind == 4 and v.kind == 2 for k, v in mapping.value), 'typed idempotency mapping')
|
||||
require([k.value for k, _ in mapping.value] == sorted(k.value for k, _ in mapping.value), 'sorted idempotency')
|
||||
require(not q.at('pending').value and not q.at('in_flight').value, 'isolated guard queue')
|
||||
require(rt.at('operations').at('enabled').value is False, 'guard operations disabled')
|
||||
ident = rt.at('next_command_id').value
|
||||
minute = command.at('issued_tick').value
|
||||
key = command.at('idempotency_key').value
|
||||
kind = 'create_service_request' if guard == 'G1' else 'register_defense_asset'
|
||||
require(command.at('command_id').kind == 2 and command.at('command_id').value == ident
|
||||
and command.at('execute_tick').value == minute
|
||||
and command.at('priority').value == 10 and command.at('command_type').value == kind
|
||||
and command.at('actor_id').value == 'player' and command.at('target_id').value == ''
|
||||
and key.startswith('p04.s5.') and key not in q.at('idempotency').plain(), 'fixed guard command binding')
|
||||
payload = command.at('payload').plain()
|
||||
if guard == 'G1':
|
||||
require(set(payload) == {'facility_id', 'request_id', 'guest_ids'}
|
||||
and payload['request_id'] == 'p04.s5.request.a'
|
||||
and payload['guest_ids'] == ['p04.s5.guest.a'], 'G1 payload')
|
||||
else:
|
||||
require(set(payload) == {'raid_id', 'asset_id', 'kind', 'max_integrity'}
|
||||
and payload['raid_id'] == 'p04.s5.guard.raid' and payload['kind'] == 'OBJECTIVE'
|
||||
and payload['max_integrity'] == 10000, 'G2 payload')
|
||||
details = dictionary([(atom(21, 'raid_id'), atom(21, 'p04.s5.raid'))]) if guard == 'G1' else dictionary([])
|
||||
result = strings_dict([
|
||||
('code', atom(4, 'VALIDATION_FAILED')), ('command_id', atom(2, ident)),
|
||||
('target_id', atom(4, '')), ('message', atom(4, MESSAGES[guard])),
|
||||
('resolution_hints', array([], 4)), ('details', details), ('is_replay', atom(1, 0))])
|
||||
accepted = q.at('accepted').value
|
||||
processed = q.at('processed').value
|
||||
for row in accepted:
|
||||
old_key = row.at('idempotency_key')
|
||||
require(old_key.kind == 4, 'accepted String key')
|
||||
if old_key.value:
|
||||
require(mapping.at(old_key.value).value == row.at('command_id').value, 'prior accepted/idempotency join')
|
||||
require(all(r.at('command_id').value < ident for r in accepted + processed), 'prior journal IDs')
|
||||
metadata = strings_dict([('command_id', atom(2, ident)), ('idempotency_key', atom(4, key))])
|
||||
idem = [(k.value, v.raw) for k, v in q.at('idempotency').value] + [(key, atom(2, ident))]
|
||||
queue = update(q, {'accepted': array([r.raw for r in accepted] + [metadata], 27),
|
||||
'processed': array([r.raw for r in processed] + [result], 27),
|
||||
'idempotency': strings_dict(sorted(idem))})
|
||||
generator = rt.at('id_generator')
|
||||
counters = generator.at('counters').plain()
|
||||
require(all(k.kind == 4 and v.kind == 2 for k, v in generator.at('counters').value), 'typed counters')
|
||||
counters['domain_event'] = counters.get('domain_event', 0) + 1
|
||||
eid = 'domain_event.%012d' % counters['domain_event']
|
||||
ids = update(generator, {'counters': strings_dict([(k, atom(2, v)) for k, v in sorted(counters.items())])})
|
||||
event = strings_dict([('event_id', atom(4, eid)), ('event_type', atom(4, 'COMMAND_FAILED')),
|
||||
('absolute_minute', atom(2, minute)), ('source_service', atom(4, 'simulation_runtime')),
|
||||
('entity_ids', array([], 4)), ('payload', result), ('causation_command_id', atom(2, ident))])
|
||||
stream = rt.at('domain_events')
|
||||
published = stream.at('published_ids').plain()
|
||||
require(eid not in published, 'new event ID')
|
||||
events = update(stream, {'published_ids': array([atom(4, k) for k in sorted(published + [eid])], 4),
|
||||
'recent_events': array(([r.raw for r in stream.at('recent_events').value] + [event])[-256:], 27)})
|
||||
runtime = update(rt, {'next_command_id': atom(2, ident + 1), 'command_queue': queue,
|
||||
'id_generator': ids, 'domain_events': events})
|
||||
envelope = update(before, {'systems': update(before.at('systems'), {'runtime': runtime})})
|
||||
return parse(envelope), parse(result)
|
||||
|
||||
|
||||
def verify(record, parse, require):
|
||||
before, command = record.at('before'), record.at('command')
|
||||
tree, result = expected(before, command, record.at('guard').value, parse, require)
|
||||
require(record.at('expected').raw == tree.raw, 'EXPECTED must independently derive from BEFORE; AFTER-copy forbidden')
|
||||
require(record.at('after').raw == tree.raw, 'whole typed guard Runtime; no business or journal masks')
|
||||
require(record.at('result').raw == result.raw, 'independent exact failed result')
|
||||
return True
|
||||
@@ -19,6 +19,9 @@ Raw, staff_check, duplicate_check = power.Raw, power.staff_check, power.duplicat
|
||||
series, require = power.series, power.require
|
||||
DEPENDENCIES = {str(p): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in (POWER_PATH, power.SERIES_PATH)}
|
||||
GUARD_PATH = Path(__file__).with_name('s5_guard_oracle.py')
|
||||
guard_oracle = module('s5_guard_oracle', GUARD_PATH)
|
||||
DEPENDENCIES[str(GUARD_PATH)] = hashlib.sha256(GUARD_PATH.read_bytes()).hexdigest()
|
||||
GUEST, REQUEST = power.GUEST, power.REQUEST
|
||||
SECOND, SECOND_REQUEST = 'p04.s5.guest.b', 'p04.s5.request.b'
|
||||
|
||||
@@ -73,6 +76,9 @@ def payment(before, after, minute):
|
||||
sale = sales(after)
|
||||
require(not sales(before) and len(sale) == 1, 'exactly one new sale')
|
||||
price = scalar(before, 'inputs', 'price')
|
||||
require(price == scalar(before, 'service', 'price_snapshot')
|
||||
and scalar(before, 'inputs', 'duration') == scalar(before, 'service', 'duration_minutes'),
|
||||
'values derived from service snapshot')
|
||||
require(price > 0 and scalar(sale[0], 'amount') == price
|
||||
and scalar(sale[0], 'absolute_minute') == minute
|
||||
and scalar(sale[0], 'source_entity_id') == scalar(after, 'inputs', 'facility_id'),
|
||||
@@ -82,6 +88,11 @@ def payment(before, after, minute):
|
||||
require(scalar(after, 'service', 'state') == 'COMPLETED'
|
||||
and scalar(after, 'service', 'completed_at') == minute, 'completed before payment')
|
||||
runtime = field(after, 'snapshot', 'systems', 'runtime')
|
||||
history = [r for r in rows(runtime, 'facilities', 'service_history')
|
||||
if scalar(r, 'service_id') == scalar(after, 'service', 'service_id')]
|
||||
require(len(history) == 1 and history[0].raw == field(after, 'service').raw, 'completed service joins typed Runtime')
|
||||
require(sum(r.raw == sale[0].raw for r in rows(runtime, 'economy', 'entries')) == 1,
|
||||
'sale joins typed Runtime ledger once')
|
||||
key = 'service.payment.' + scalar(after, 'service', 'service_id')
|
||||
idem = [r for r in rows(runtime, 'economy', 'idempotency') if scalar(r, 'key') == key]
|
||||
require(len(idem) == 1 and scalar(idem[0], 'transaction_id') == scalar(sale[0], 'transaction_id'),
|
||||
@@ -106,7 +117,7 @@ def destruction_command(points, phases):
|
||||
return {'destruction_stop': {'charges': 0}, 'command_destruction_before_completion': True}
|
||||
|
||||
|
||||
def destruction_raid(points, phases):
|
||||
def destruction_raid_r40(points, phases):
|
||||
before, end = points['before'], points['held']
|
||||
n = scalar(before, 'inputs', 'boundary')
|
||||
pre = phase(phases, 'INCIDENTS_AND_EVENTS', n)
|
||||
@@ -127,6 +138,138 @@ def destruction_raid(points, phases):
|
||||
return {'raid_sale_before_destruction': True, 'raid_unfinished_release': True}
|
||||
|
||||
|
||||
def destruction_raid(points, phases):
|
||||
before = points['before']
|
||||
n = scalar(before, 'inputs', 'boundary')
|
||||
due = rows(before, 'inputs', 'recovery', 'due_rounds')
|
||||
require(len(due) == 1 and scalar(due[0], 'earliest_minute') == n
|
||||
and scalar(due[0], 'raid_id') == 'p04.s5.raid', 'idle OBJECTIVE due exactly N')
|
||||
pre = phase(phases, 'INCIDENTS_AND_EVENTS', n)
|
||||
require(scalar(pre, 'facility', 'durability') > 0, 'idle breach not before incidents')
|
||||
for point in (phase(phases, 'FACILITY_QUEUES', n), points['boundary'], points['held']):
|
||||
require(scalar(point, 'facility', 'durability') == 0 and scalar(point, 'facility', 'state') == 'BROKEN'
|
||||
and scalar(point, 'facility', 'suspension_reason') == 'DESTROYED', 'automatic idle destruction in INCIDENTS_AND_EVENTS')
|
||||
require(not rows(point, 'reservations') and field(point, 'service').plain() == {}
|
||||
and scalar(point, 'active_services') == 0
|
||||
and not rows(point, 'snapshot', 'systems', 'runtime', 'facilities', 'requests')
|
||||
and all(v.plain() == {} for _, v in field(point, 'requests').value), 'idle no requests/reservations/orphans')
|
||||
require(not any(scalar(r, 'category') == 'SERVICE_SALE' for r in rows(point, 'ledger')),
|
||||
'idle raid zero SERVICE_SALE')
|
||||
return {'idle_objective_breach': True, 'destruction_stop': {'charges': 0}}
|
||||
|
||||
|
||||
def destruction_in_flight(points, phases):
|
||||
before, boundary, end = points['before'], points['boundary'], points['held']
|
||||
n = scalar(before, 'inputs', 'boundary')
|
||||
pre = phase(phases, 'INCIDENTS_AND_EVENTS', n)
|
||||
post = phase(phases, 'FACILITY_QUEUES', n)
|
||||
require(scalar(pre, 'facility', 'durability') > 0 and scalar(pre, 'service', 'state') == 'ACTIVE'
|
||||
and scalar(pre, 'service', 'elapsed_minutes') < scalar(pre, 'service', 'duration_minutes'),
|
||||
'in-flight completion phase precedes destruction')
|
||||
require(scalar(post, 'facility', 'durability') == 0 and scalar(post, 'facility', 'state') == 'BROKEN'
|
||||
and scalar(boundary, 'service', 'state') == 'ACTIVE', 'event breaks active service at N')
|
||||
require(scalar(end, 'minute') == n + 1 and scalar(end, 'service', 'state') == 'FAILED'
|
||||
and scalar(end, 'service', 'failure_reason') == 'BROKEN', 'N+1 in-flight failure')
|
||||
for p in (boundary, end):
|
||||
require(not sales(p) and field(p, 'wallets').raw == field(before, 'wallets').raw, 'in-flight zero charge')
|
||||
release(before, end, REQUEST, n + 1)
|
||||
return {'in_flight_destruction': True, 'destruction_stop': {'charges': 0}}
|
||||
|
||||
|
||||
def shift_completion(points, phases):
|
||||
before, end, held = points['before'], points['boundary'], points['held']
|
||||
require(scalar(before, 'inputs', 'boundary') == 1080, 'DAY end exactly 1080')
|
||||
require(scalar(before, 'minute') == 1079 and scalar(before, 'service', 'state') == 'ACTIVE'
|
||||
and scalar(before, 'service', 'started_at') + scalar(before, 'service', 'duration_minutes') == 1080
|
||||
and scalar(before, 'service', 'elapsed_minutes') == scalar(before, 'service', 'duration_minutes') - 1,
|
||||
'natural snapshot duration aligns completion to 1080')
|
||||
for point, label in [(before, 'first-before-17-59'), (end, 'first-18-00-tick-complete'),
|
||||
(held, 'first-18-01-tick-complete'),
|
||||
(phase(phases, 'FACILITY_COMPLETION', 1080), 'first-18-00-before-facility'),
|
||||
(phase(phases, 'VITALS_AND_ENVIRONMENT', 1080), 'first-18-00-after-facility'),
|
||||
(phase(phases, 'PUBLISH_SNAPSHOT', 1080), 'first-18-00-after-staff')]:
|
||||
require(scalar(point, 'order_label') == label, 'historical shift observation label: ' + label)
|
||||
require(not sales(phase(phases, 'FACILITY_COMPLETION', 1080)), 'not charged before FACILITY_COMPLETION')
|
||||
sale = payment(before, phase(phases, 'VITALS_AND_ENVIRONMENT', 1080), 1080)
|
||||
payment(before, end, 1080)
|
||||
payment(before, held, 1080)
|
||||
require(scalar(held, 'minute') == 1081 and field(end, 'service').raw == field(held, 'service').raw,
|
||||
'1081 no second completion')
|
||||
receipts = [r for r in rows(end, 'receipts') if scalar(r, 'id') == scalar(sale, 'transaction_id')]
|
||||
require(len(receipts) == 1 and scalar(receipts[0], 'phase') == 'FACILITY_COMPLETION', 'sale before handover')
|
||||
staff = rows(receipts[0], 'staff_at_receipt')
|
||||
ids = field(before, 'service', 'staff_ids').plain()
|
||||
require(ids and [scalar(r, 'staff_id') for r in staff] == ids, 'DAY staff captured at receipt')
|
||||
shifts = rows(before, 'snapshot', 'systems', 'runtime', 'staff', 'shifts')
|
||||
final_shifts = rows(end, 'snapshot', 'systems', 'runtime', 'staff', 'shifts')
|
||||
final_staff = rows(end, 'snapshot', 'systems', 'runtime', 'staff', 'staff')
|
||||
for worker in staff:
|
||||
ident = scalar(worker, 'staff_id')
|
||||
shift = [s for s in shifts if scalar(s, 'shift_instance_id') == scalar(worker, 'shift_id')]
|
||||
require(len(shift) == 1 and scalar(shift[0], 'end_at') == 1080 and scalar(shift[0], 'template_id') == 'DAY'
|
||||
and scalar(worker, 'state') != 'OFF_DUTY', 'DAY not NIGHT staff before handover')
|
||||
after = [s for s in final_shifts if scalar(s, 'shift_instance_id') == scalar(worker, 'shift_id')]
|
||||
require(len(after) == 1 and scalar(after[0], 'actual_ended_at') == scalar(after[0], 'end_at') == 1080,
|
||||
'DAY shift ended after receipt')
|
||||
workers = [s for s in final_staff if scalar(s, 'staff_id') == ident]
|
||||
require(len(workers) == 1 and scalar(workers[0], 'state') == 'OFF_DUTY', 'DAY now OFF_DUTY')
|
||||
return {'shift_completion': {'minute': 1080, 'completions': 1, 'charges': 1, 'completion_before_shift': True}}
|
||||
|
||||
|
||||
def paused_midnight(points, phases):
|
||||
before, end = points['before'], points['recovered']
|
||||
n = scalar(before, 'inputs', 'boundary')
|
||||
require(n > 0 and n % 1440 == 0, 'paused midnight multiple')
|
||||
for p in (before, phase(phases, 'TIME_BOUNDARIES', n), phase(phases, 'MANA', n), points['boundary'], points['held']):
|
||||
require(scalar(p, 'service', 'state') == 'PAUSED'
|
||||
and scalar(p, 'service', 'interruption_reason') == 'RELATED_FACILITY_INACTIVE'
|
||||
and scalar(p, 'service', 'elapsed_minutes') == scalar(before, 'service', 'elapsed_minutes'),
|
||||
'legitimate pause unchanged through DAILY_CLOSE')
|
||||
require(field(p, 'reservations').raw == field(before, 'reservations').raw
|
||||
and field(p, 'requests', REQUEST).raw == field(before, 'requests', REQUEST).raw,
|
||||
'identical active reservations and preserved request')
|
||||
require(not sales(p) and field(p, 'wallets').raw == field(before, 'wallets').raw, 'no paused sale')
|
||||
active = rows(before, 'reservations')
|
||||
require(len(active) == 1 and scalar(active[0], 'state') == 'ACTIVE', 'one active meal reservation')
|
||||
post = phase(phases, 'MANA', n)
|
||||
summaries = rows(post, 'snapshot', 'systems', 'runtime', 'operations', 'coordinator', 'day_summaries')
|
||||
summaries = [s for s in summaries if scalar(s, 'closed_at') == n]
|
||||
require(len(summaries) == 1, 'one DAILY_CLOSE during TIME_BOUNDARIES')
|
||||
settlement_rows = [r for r in rows(post, 'ledger') if scalar(r, 'absolute_minute') == n]
|
||||
upkeep = [r for r in settlement_rows if scalar(r, 'category') == 'MAINTENANCE_PAYMENT']
|
||||
require(upkeep and len({scalar(r, 'transaction_id') for r in upkeep}) == 1, 'one paused upkeep settlement')
|
||||
require(len({scalar(r, 'source_entity_id') for r in upkeep}) == len(upkeep), 'one upkeep row per owned facility')
|
||||
for row in upkeep:
|
||||
accrual = [r for r in rows(post, 'ledger') if scalar(r, 'category') == 'MAINTENANCE_ACCRUAL'
|
||||
and scalar(r, 'transaction_id') == scalar(row, 'transaction_id')
|
||||
and scalar(r, 'source_entity_id') == scalar(row, 'source_entity_id')
|
||||
and scalar(r, 'amount') == scalar(row, 'amount') and scalar(r, 'absolute_minute') == n - 1]
|
||||
require(len(accrual) == 1, 'paused upkeep accrual joined once')
|
||||
wages = [r for r in settlement_rows if scalar(r, 'category') == 'WAGE']
|
||||
require(len(wages) == 1, 'one paused payroll transaction')
|
||||
payroll = [r for r in rows(post, 'snapshot', 'systems', 'runtime', 'staff', 'payroll_settlements')
|
||||
if scalar(r, 'settlement_id') == scalar(wages[0], 'source_entity_id')]
|
||||
require(len(payroll) == 1 and scalar(payroll[0], 'day') == n // 1440, 'paused payroll day/ledger join')
|
||||
paid = [r for r in rows(payroll[0], 'rows') if scalar(r, 'payment') > 0]
|
||||
require(paid and len({scalar(r, 'staff_id') for r in paid}) == len(paid)
|
||||
and sum(scalar(r, 'payment') for r in paid) == scalar(wages[0], 'amount'), 'paused payroll paid once')
|
||||
for p in (points['boundary'], points['held'], end):
|
||||
require([r.raw for r in rows(p, 'ledger') if scalar(r, 'absolute_minute') == n] == [r.raw for r in settlement_rows],
|
||||
'no double settlement or midnight sale')
|
||||
current = [s for s in rows(p, 'snapshot', 'systems', 'runtime', 'operations', 'coordinator', 'day_summaries')
|
||||
if scalar(s, 'closed_at') == n]
|
||||
require([s.raw for s in current] == [s.raw for s in summaries], 'settlement summary posted once')
|
||||
completion = scalar(end, 'service', 'completed_at')
|
||||
require(n < completion < n + 1440, 'recovery in new day')
|
||||
payment(before, end, completion)
|
||||
consumed = rows(end, 'reservations')
|
||||
require(len(consumed) == 1 and scalar(consumed[0], 'reservation_id') == scalar(active[0], 'reservation_id')
|
||||
and scalar(consumed[0], 'state') == 'CONSUMED' and field(end, 'requests', REQUEST).plain() == {},
|
||||
'consumed original reservation no orphan')
|
||||
return {'paused_midnight': {'orphan_reservations': 0, 'crossed_midnight_while_paused': True,
|
||||
'stopped_charges': 0, 'completions': 1, 'charges': 1}}
|
||||
|
||||
|
||||
def midnight_day(points, phases):
|
||||
before, end, replay = points['before'], points['boundary'], points['recalled']
|
||||
n = scalar(before, 'inputs', 'boundary')
|
||||
@@ -190,7 +333,8 @@ def exit_order(points, phases):
|
||||
|
||||
|
||||
DERIVERS = {'S5C_DESTRUCTION_COMMAND': destruction_command, 'S5D_DESTRUCTION_RAID': destruction_raid,
|
||||
'S5E_MIDNIGHT': midnight_day, 'S5F_EXIT': exit_order}
|
||||
'S5D_IN_FLIGHT': destruction_in_flight, 'S5G_SHIFT_1080': shift_completion,
|
||||
'S5H_PAUSED_MIDNIGHT': paused_midnight, 'S5E_MIDNIGHT': midnight_day, 'S5F_EXIT': exit_order}
|
||||
|
||||
|
||||
def derive(root, writer_sha, reader_sha):
|
||||
@@ -209,8 +353,31 @@ def derive(root, writer_sha, reader_sha):
|
||||
phases = [raw.typed(r) for r in branch['phases']]
|
||||
raw.typed(branch['commands'])
|
||||
for p in points.values():
|
||||
staff_check(p, field(p, 'inputs', 'recovery'))
|
||||
if case not in ('S5D_DESTRUCTION_RAID', 'S5G_SHIFT_1080'):
|
||||
staff_check(p, field(p, 'inputs', 'recovery'))
|
||||
if case == 'S5G_SHIFT_1080':
|
||||
staff_check(points['before'], field(points['before'], 'inputs', 'recovery'))
|
||||
result[case] = function(points, phases)
|
||||
expected_guards = {'G1'} if case == 'S5D_DESTRUCTION_RAID' else {'G2'} if case == 'S5D_IN_FLIGHT' else set()
|
||||
require(set(branch['guards']) == expected_guards, 'complete fixed guard roster')
|
||||
for label, ref in branch['guards'].items():
|
||||
guard = raw.typed(ref)
|
||||
require(scalar(guard, 'guard') == label, 'guard label binding')
|
||||
facility_id = scalar(points['before'], 'inputs', 'facility_id')
|
||||
payload_field = 'facility_id' if label == 'G1' else 'asset_id'
|
||||
require(scalar(guard, 'command', 'payload', payload_field) == facility_id, 'guard binds case facility')
|
||||
facilities = rows(guard, 'before', 'systems', 'runtime', 'facilities', 'instances')
|
||||
facility = [f for f in facilities if scalar(f, 'instance_id') == facility_id]
|
||||
require(len(facility) == 1, 'guard facility exists in BEFORE')
|
||||
if label == 'G2':
|
||||
require(rows(facility[0], 'active_services'), 'G2 independently busy BEFORE')
|
||||
else:
|
||||
raids = rows(guard, 'before', 'systems', 'runtime', 'defense', 'raids')
|
||||
raid = [r for r in raids if scalar(r, 'raid_id') == 'p04.s5.raid']
|
||||
require(len(raid) == 1 and scalar(raid[0], 'state') in ('PREPARING', 'ACTIVE', 'BETWEEN_WAVES')
|
||||
and any(scalar(asset, 'asset_id') == facility_id for asset in rows(raid[0], 'assets')),
|
||||
'G1 independently non-terminal Defense ownership')
|
||||
result[case][label] = guard_oracle.verify(guard, series.variant, require)
|
||||
for label, ref in replay['restored'].items():
|
||||
require(field(raw.typed(ref), 'snapshot').raw == field(points[label], 'snapshot').raw,
|
||||
'whole typed formal restore')
|
||||
@@ -224,6 +391,12 @@ def derive(root, writer_sha, reader_sha):
|
||||
require([raw.typed(r).raw for r in replay['phases']] == expected_phases, 'complete phase replay')
|
||||
result[case]['formal_restore_verified'] = True
|
||||
if case == 'S5E_MIDNIGHT':
|
||||
for reference in (branch['resource_replay'], replay['resource_replay']):
|
||||
record = raw.typed(reference)
|
||||
require(field(record, 'before').raw == field(record, 'after').raw == field(points['boundary'], 'snapshot').raw
|
||||
and scalar(record, 'result', 'is_replay') is True and scalar(record, 'result', 'code') == 'OK'
|
||||
and scalar(record, 'minute') == n, 'public advance_to after formal boundary restore')
|
||||
result[case]['same_minute_replay'] = {'completions': 1, 'charges': 1, 'duplicate_generations': 0, 'same_minute_replayed': True}
|
||||
result[case]['save_restore'] = {'settlements': 1, 'duplicate_payments': 0, 'formal_save_restored': True}
|
||||
return {'cases': result, 'dependency_sha256': DEPENDENCIES, 'raw_sha256': raw.hashes,
|
||||
'pending': ['patience_release', 'paused_midnight'], 'formal_acceptance': False}
|
||||
'pending': ['patience_release'], 'formal_acceptance': False}
|
||||
|
||||
+137
-5
@@ -13,13 +13,110 @@ mutation = a.module('order_typed_mutation', Path(__file__).with_name('s5_power_a
|
||||
BUILD = Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p04-01/.runtime/P04-01/build')
|
||||
|
||||
|
||||
def guard_mutations(record):
|
||||
"""All typed AFTER nodes are tested, including every changed journal field.
|
||||
|
||||
Also copy each corrupted AFTER into EXPECTED: the independent derivation
|
||||
must still reject it. No native evidence is produced by these controls.
|
||||
"""
|
||||
oracle = a.guard_oracle
|
||||
a.require(oracle.verify(record, a.series.variant, a.require), 'guard control prerequisite')
|
||||
checks = []
|
||||
after = record.at('after')
|
||||
|
||||
def visit(node, path):
|
||||
replacements = []
|
||||
if node.kind in (4, 21):
|
||||
replacements = [('value', oracle.atom(node.kind, node.value + '.mutated')),
|
||||
('string-type', oracle.atom(21 if node.kind == 4 else 4, node.value))]
|
||||
elif node.kind == 2:
|
||||
replacements = [('value', oracle.atom(2, node.value + 1)),
|
||||
('int-float', struct.pack('<If', 3, float(node.value)))]
|
||||
elif node.kind == 1:
|
||||
replacements = [('bool', oracle.atom(1, not node.value))]
|
||||
elif node.kind == 28:
|
||||
start = 12 if node.raw[:4] == struct.pack('<I', 65564) else 8
|
||||
if node.value:
|
||||
replacements.append(('extra-row', node.raw[:start-4] + struct.pack('<I', len(node.value)+1)
|
||||
+ node.raw[start:] + node.value[-1].raw))
|
||||
if len(node.value) > 1:
|
||||
replacements.append(('unsorted', node.raw[:start] + b''.join(r.raw for r in reversed(node.value))))
|
||||
for i, child in enumerate(node.value):
|
||||
visit(child, path + '/' + str(i))
|
||||
elif node.kind == 27:
|
||||
if len(node.value) > 1:
|
||||
replacements.append(('unsorted', oracle.dictionary([(k.raw, v.raw) for k, v in reversed(node.value)])))
|
||||
for key, child in node.value:
|
||||
visit(key, path + '/key:' + str(key.value))
|
||||
visit(child, path + '/' + str(key.value))
|
||||
for label, encoded in replacements:
|
||||
changed_after = mutation.replace(after, node, encoded)
|
||||
for copied in (False, True):
|
||||
changed_record = mutation.replace(record, after, changed_after)
|
||||
try:
|
||||
changed = a.series.variant(changed_record)
|
||||
if copied:
|
||||
changed_record = mutation.replace(changed, changed.at('expected'), changed_after)
|
||||
oracle.verify(a.series.variant(changed_record), a.series.variant, a.require)
|
||||
except ValueError:
|
||||
checks.append(path + ':' + label + (':AFTER-copy' if copied else ''))
|
||||
continue
|
||||
raise ValueError('guard mutation accepted: ' + path + ':' + label)
|
||||
visit(after, 'after')
|
||||
return checks
|
||||
|
||||
|
||||
def pure_controls():
|
||||
"""Source-shaped synthetic serializer units, never Writer/Reader positives."""
|
||||
o = a.guard_oracle
|
||||
atom, d, arr = o.atom, o.strings_dict, o.array
|
||||
prior = lambda ident: d([('code', atom(4, 'OK')), ('command_id', atom(2, ident)),
|
||||
('target_id', atom(4, '')), ('message', atom(4, '')),
|
||||
('resolution_hints', arr([], 4)), ('details', d([])), ('is_replay', atom(1, 0))])
|
||||
q = d([('version', atom(2, 2)), ('pending', arr([], 27)), ('in_flight', arr([], 27)),
|
||||
('processed', arr([prior(1), prior(2)], 27)),
|
||||
('idempotency', d([('a', atom(2, 1)), ('b', atom(2, 2))])),
|
||||
('accepted', arr([d([('command_id', atom(2, i)), ('idempotency_key', atom(4, k))])
|
||||
for i, k in [(1, 'a'), (2, 'b')]], 27))])
|
||||
runtime = d([('next_command_id', atom(2, 3)), ('command_queue', q),
|
||||
('id_generator', d([('version', atom(2, 1)), ('counters', d([('domain_event', atom(2, 2))]))])),
|
||||
('domain_events', d([('version', atom(2, 1)), ('published_ids', arr([atom(4, 'domain_event.000000000001'), atom(4, 'domain_event.000000000002')], 4)), ('recent_events', arr([], 27))])),
|
||||
('operations', d([('enabled', atom(1, 0))])),
|
||||
('business', d([('wallet', atom(2, 100)), ('reservations', arr([], 27))]))])
|
||||
before = a.series.variant(d([('systems', d([('runtime', runtime)]))]))
|
||||
results = {}
|
||||
for guard in ('G1', 'G2'):
|
||||
payload = (o.dictionary([(atom(21, 'facility_id'), atom(21, 'facility.1')),
|
||||
(atom(21, 'request_id'), atom(21, a.REQUEST)),
|
||||
(atom(21, 'guest_ids'), arr([atom(21, a.GUEST)]))]) if guard == 'G1' else
|
||||
o.dictionary([(atom(21, 'raid_id'), atom(21, 'p04.s5.guard.raid')),
|
||||
(atom(21, 'asset_id'), atom(21, 'facility.1')),
|
||||
(atom(21, 'kind'), atom(21, 'OBJECTIVE')),
|
||||
(atom(21, 'max_integrity'), atom(2, 10000))]))
|
||||
command = a.series.variant(d([('command_id', atom(2, 3)), ('issued_tick', atom(2, 959)),
|
||||
('execute_tick', atom(2, 959)), ('priority', atom(2, 10)),
|
||||
('command_type', atom(4, 'create_service_request' if guard == 'G1' else 'register_defense_asset')),
|
||||
('actor_id', atom(4, 'player')), ('target_id', atom(4, '')),
|
||||
('payload', payload), ('idempotency_key', atom(4, 'p04.s5.unit.3'))]))
|
||||
expected, result = o.expected(before, command, guard, a.series.variant, a.require)
|
||||
record = a.series.variant(d([('before', before.raw), ('command', command.raw), ('guard', atom(4, guard)),
|
||||
('expected', expected.raw), ('after', expected.raw), ('result', result.raw)]))
|
||||
results[guard] = guard_mutations(record)
|
||||
return {'synthetic_serializer_controls_only': True, 'negative_controls': results, 'formal_acceptance': False}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--raw-root', type=Path, required=True)
|
||||
parser.add_argument('--writer-sha256', required=True)
|
||||
parser.add_argument('--reader-sha256', required=True)
|
||||
parser.add_argument('--scratch', type=Path, required=True)
|
||||
parser.add_argument('--pure-only', action='store_true')
|
||||
parser.add_argument('--raw-root', type=Path)
|
||||
parser.add_argument('--writer-sha256')
|
||||
parser.add_argument('--reader-sha256')
|
||||
parser.add_argument('--scratch', type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.pure_only:
|
||||
print(json.dumps(pure_controls(), sort_keys=True))
|
||||
return
|
||||
a.require(all((args.raw_root, args.writer_sha256, args.reader_sha256, args.scratch)), 'explicit fresh raw arguments')
|
||||
scratch = args.scratch
|
||||
a.require(scratch.is_absolute() and not scratch.exists() and not scratch.is_symlink(), 'explicit absent scratch')
|
||||
a.require(scratch.parent.resolve(strict=True).is_relative_to(BUILD.resolve(strict=True)), 'scratch inside BUILD')
|
||||
@@ -66,7 +163,11 @@ def main():
|
||||
raid = 'S5D_DESTRUCTION_RAID'
|
||||
points, phases = cases[raid]
|
||||
pre = a.phase(phases, 'INCIDENTS_AND_EVENTS', a.scalar(points['before'], 'inputs', 'boundary'))
|
||||
negative('raid-sale-missing', raid, pre, a.field(a.sales(pre)[0], 'category'), mutation.scalar(4, 'REMOVED_SALE'))
|
||||
negative('idle-breach-too-early', raid, pre, a.field(pre, 'facility', 'durability'), mutation.scalar(2, 0))
|
||||
for case in ('S5D_DESTRUCTION_RAID', 'S5D_IN_FLIGHT'):
|
||||
branch = raw.reference(writer['branches'][case])
|
||||
for label, reference in branch['guards'].items():
|
||||
checks.extend(label + ':' + name for name in guard_mutations(raw.typed(reference)))
|
||||
command = 'S5C_DESTRUCTION_COMMAND'
|
||||
points, phases = cases[command]
|
||||
end = points['held']
|
||||
@@ -83,6 +184,37 @@ def main():
|
||||
pre = a.phase(phases, 'FACILITY_COMPLETION', n)
|
||||
reservation = next(r for r in a.rows(pre, 'reservations') if a.scalar(r, 'requester_id') == a.SECOND_REQUEST)
|
||||
negative('command-exit-after-start', exit_case, pre, a.field(reservation, 'terminal_at'), mutation.scalar(2, n + 1))
|
||||
shift = 'S5G_SHIFT_1080'
|
||||
points, phases = cases[shift]
|
||||
end = points['boundary']
|
||||
receipt = next(r for r in a.rows(end, 'receipts') if a.scalar(r, 'id') == a.scalar(a.sales(end)[0], 'transaction_id'))
|
||||
negative('sale-after-handover', shift, end, a.field(receipt, 'phase'), mutation.scalar(4, 'AI_DECISIONS'))
|
||||
staff = a.field(receipt, 'staff_at_receipt')
|
||||
negative('empty-staff-at-receipt', shift, end, staff, a.guard_oracle.array([]))
|
||||
negative('night-staff-at-receipt', shift, end, a.field(staff.value[0], 'shift_id'), mutation.scalar(4, 'NIGHT'))
|
||||
paused = 'S5H_PAUSED_MIDNIGHT'
|
||||
points, phases = cases[paused]
|
||||
end = points['boundary']
|
||||
reservation = a.rows(end, 'reservations')[0]
|
||||
negative('reservation-released-at-midnight', paused, end, a.field(reservation, 'state'), mutation.scalar(4, 'RELEASED'))
|
||||
negative('reservation-changed-at-midnight', paused, end, a.field(reservation, 'quantity'), mutation.scalar(2, a.scalar(reservation, 'quantity') + 1))
|
||||
negative('paused-elapsed-typed-different', paused, end, a.field(end, 'service', 'elapsed_minutes'), struct.pack('<If', 3, float(a.scalar(end, 'service', 'elapsed_minutes'))))
|
||||
ledger = a.field(end, 'ledger')
|
||||
sale = a.sales(points['recovered'])[0]
|
||||
start = 12 if ledger.raw[:4] == struct.pack('<I', 65564) else 8
|
||||
def appended(row):
|
||||
return ledger.raw[:start-4] + struct.pack('<I', len(ledger.value)+1) + ledger.raw[start:] + row.raw
|
||||
negative('sale-while-paused', paused, end, ledger, appended(sale))
|
||||
settlement = next(r for r in ledger.value if a.scalar(r, 'absolute_minute') == a.scalar(end, 'inputs', 'boundary'))
|
||||
negative('double-paused-settlement', paused, end, ledger, appended(settlement))
|
||||
points, phases = cases[midnight]
|
||||
replay = points['recalled']
|
||||
counter = a.field(replay, 'snapshot', 'systems', 'runtime', 'next_command_id')
|
||||
negative('replay-extra-id', midnight, replay, counter, mutation.scalar(2, counter.value + 1))
|
||||
negative('replay-json-equal-type-difference', midnight, replay, counter, struct.pack('<If', 3, float(counter.value)))
|
||||
ledger = a.field(replay, 'snapshot', 'systems', 'runtime', 'economy', 'entries')
|
||||
start = 12 if ledger.raw[:4] == struct.pack('<I', 65564) else 8
|
||||
negative('replay-extra-row', midnight, replay, ledger, ledger.raw[:start-4] + struct.pack('<I', len(ledger.value)+1) + ledger.raw[start:] + ledger.value[-1].raw)
|
||||
print(json.dumps({'positive_cases': list(positive['cases']), 'negative_controls': checks,
|
||||
'formal_acceptance': False}, sort_keys=True))
|
||||
|
||||
|
||||
@@ -100,9 +100,10 @@ def derive(root, writer_sha, reader_sha):
|
||||
writer = json.loads(raw.pin(root / "writer.json", writer_sha), object_pairs_hook=unique)
|
||||
reader = json.loads(raw.pin(root / "reader.json", reader_sha), object_pairs_hook=unique)
|
||||
require(writer["pid"] != reader["pid"] and writer["profile"] != reader["profile"], "independent reader required")
|
||||
expected = set(CASES) | {"S5C_DESTRUCTION_COMMAND", "S5D_DESTRUCTION_RAID", "S5E_MIDNIGHT", "S5F_EXIT"}
|
||||
expected = set(CASES) | {"S5C_DESTRUCTION_COMMAND", "S5D_DESTRUCTION_RAID", "S5D_IN_FLIGHT", "S5E_MIDNIGHT", "S5F_EXIT", "S5G_SHIFT_1080", "S5H_PAUSED_MIDNIGHT"}
|
||||
historical_roster = set(CASES) | {"S5C_DESTRUCTION_COMMAND", "S5D_DESTRUCTION_RAID", "S5E_MIDNIGHT", "S5F_EXIT"}
|
||||
require(set(writer["branches"]) == set(reader["branches"]) and
|
||||
set(writer["branches"]) in (set(CASES), expected), "exact pair or full case table roster")
|
||||
set(writer["branches"]) in (set(CASES), historical_roster, expected), "exact pair or full case table roster")
|
||||
output = {}
|
||||
for case in CASES:
|
||||
branch = raw.reference(writer["branches"][case])
|
||||
|
||||
Reference in New Issue
Block a user