Compare commits

..
6 Commits
Author SHA1 Message Date
Claude Opus 5.5 (Desktop) 48d3b0023b Complete original P05-02 acceptance and keep P05 residuals open
P05-02 (all expense categories and front/back funds) is formally completed:
S 8954914 -> Q cb183fd (same tree), Runner completed 8/8 with validation,
review, integration and final passed, and a separate fresh Opus PASS for
F1-F8 (high/medium 0, low 3). Measurements were taken on S; Q was
re-verified by content and is not relabelled as measured. Original
completion moves from 23 to 24 of 88; packages stay 4 of 30.

Still open: the P05 package (P05-03), ordinary-play back_gold (P09/P11
remaining_paid_role), two UI lows deferred under the feature-first
instruction, severance, Windows and whole-game acceptance. Earlier
failures and changes_requested reviews are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:52:34 +09:00
Agent Control Runner cb183fd127 Merge commit '895491456477b42a74e59fa239bea86c4fb9aa33' into HEAD 2026-10-23 22:38:54 +09:00
Agent Control Runner 8954914564 P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件) 2026-10-23 21:14:42 +09:00
Agent Control Runner d65e3da9f3 P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件) 2026-10-23 19:42:50 +09:00
Agent Control Runner 37deb6e877 P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件) 2026-10-23 16:12:07 +09:00
Agent Control Runner 7808ca76db P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件) 2026-10-23 16:02:54 +09:00
29 changed files with 3215 additions and 66 deletions
+249 -14
View File
@@ -15,6 +15,8 @@ const STARTER_MANA_OUTPUT := 100
const STARTER_MANA_SOURCE := &"world_tree.mana_source" const STARTER_MANA_SOURCE := &"world_tree.mana_source"
const STARTER_MANA_BUS := &"world_tree.mana_bus" const STARTER_MANA_BUS := &"world_tree.mana_bus"
const PLAYER_CASH_ACCOUNT := &"PLAYER_CASH" const PLAYER_CASH_ACCOUNT := &"PLAYER_CASH"
## The settlement.funds report definition (see _daily_fund_report).
const FUND_REPORT_VERSION := 2
const CONSTRUCTION_EXPENSE_ACCOUNT := &"CONSTRUCTION_EXPENSE" const CONSTRUCTION_EXPENSE_ACCOUNT := &"CONSTRUCTION_EXPENSE"
const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE" const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE"
const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE" const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE"
@@ -909,6 +911,9 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
return false return false
if not FacilityUpkeepSettlement.prepare_accounts(restored_economy) or not restored_upkeep.start_at(restored_minute)[&"ok"]: if not FacilityUpkeepSettlement.prepare_accounts(restored_economy) or not restored_upkeep.start_at(restored_minute)[&"ok"]:
return false return false
# A12: a save made before the funds were split reads every Gold as front funds.
if not FinancePolicy.prepare_accounts(restored_economy):
return false
var saved_resources: Variant = snapshot[&"game_state"].get(&"resources", null) var saved_resources: Variant = snapshot[&"game_state"].get(&"resources", null)
if not saved_resources is Dictionary \ if not saved_resources is Dictionary \
or int(saved_resources.get(&"gold", saved_resources.get("gold", -1))) \ or int(saved_resources.get(&"gold", saved_resources.get("gold", -1))) \
@@ -1935,11 +1940,14 @@ func _operations_closed_arrears_total() -> int:
var last_close := 0 var last_close := 0
if operations_coordinator != null and operations_coordinator.get_open_financial_day() > 1: if operations_coordinator != null and operations_coordinator.get_open_financial_day() > 1:
last_close = operations_coordinator.get_daily_close_minute(operations_coordinator.get_open_financial_day() - 1) last_close = operations_coordinator.get_daily_close_minute(operations_coordinator.get_open_financial_day() - 1)
return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_through(last_close) return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_through(last_close) \
+ _finance_arrears_through(economy_service, last_close)
func _operations_total_arrears() -> int: func _operations_total_arrears() -> int:
return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_total() return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_total() \
- economy_service.get_balance(FinancePolicy.RANK_TAX_PAYABLE) \
- economy_service.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE)
func _advance_operations_time(absolute_minute: int) -> void: func _advance_operations_time(absolute_minute: int) -> void:
@@ -2504,6 +2512,13 @@ func _operations_midnight_upkeep_boundary(payload: Dictionary, phase: StringName
var prepared := _run_operations_test_boundary_hook(&"MIDNIGHT_UPKEEP", payload, OPERATIONS_TEST_PREPARE_PHASE, token) var prepared := _run_operations_test_boundary_hook(&"MIDNIGHT_UPKEEP", payload, OPERATIONS_TEST_PREPARE_PHASE, token)
if not bool(prepared.get(&"ok", false)): if not bool(prepared.get(&"ok", false)):
return prepared return prepared
var midnight_due := _upkeep_debt_through(int(minute))
if midnight_due < 0:
return {&"ok": false, &"reason": &"UPKEEP_DUE_PREVIEW_FAILED"}
var midnight_top_up := top_up_front_fund(midnight_due, int(minute), StringName("finance.back_top_up.midnight.%d" % int(minute)))
if not midnight_top_up.is_success() or (int(midnight_top_up.details.get(&"amount", 0)) > 0 \
and not _stage_operations_economy_notification(midnight_top_up)):
return {&"ok": false, &"reason": &"BACK_FUND_TOP_UP_FAILED"}
var result := _settle_upkeep_through(int(minute)) var result := _settle_upkeep_through(int(minute))
if not bool(result[&"ok"]): if not bool(result[&"ok"]):
return result return result
@@ -2524,10 +2539,29 @@ func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dicti
if not bool(prepare_hook.get(&"ok", false)): if not bool(prepare_hook.get(&"ok", false)):
return prepare_hook return prepare_hook
var day := int(payload[&"day"]) var day := int(payload[&"day"])
var boundary_minute := int(payload[&"boundary_minute"])
var preview := staff_service.preview_daily_payroll(day) var preview := staff_service.preview_daily_payroll(day)
if not bool(preview.get(&"ok", false)) \ if not bool(preview.get(&"ok", false)) \
or not preview.get(&"rows", null) is Array: or not preview.get(&"rows", null) is Array:
return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_FAILED"} return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_FAILED"}
# A09: the hidden fund first covers what the front fund cannot pay of the day's
# wages, upkeep, rank tax and loan interest (arrears included).
var finance_due := _daily_finance_due(economy_service, int(payload[&"period_start_minute"]), boundary_minute)
if not bool(finance_due[&"ok"]):
return finance_due
var payroll_due := 0
for raw_due: Variant in preview[&"rows"]:
if raw_due is Dictionary and typeof((raw_due as Dictionary).get(&"total_due", null)) == TYPE_INT:
payroll_due += maxi(int(raw_due[&"total_due"]), 0)
var upkeep_due := _upkeep_debt_through(boundary_minute)
if upkeep_due < 0:
return {&"ok": false, &"reason": &"UPKEEP_DUE_PREVIEW_FAILED"}
var top_up := top_up_front_fund(payroll_due + upkeep_due + int(finance_due[&"due_total"]), boundary_minute,
StringName("finance.back_top_up.day.%08d" % day))
if not top_up.is_success():
return {&"ok": false, &"reason": &"BACK_FUND_TOP_UP_FAILED", &"result": top_up.to_dictionary()}
if int(top_up.details.get(&"amount", 0)) > 0 and not _stage_operations_economy_notification(top_up):
return {&"ok": false, &"reason": &"BACK_FUND_TOP_UP_NOTIFICATION_STAGE_FAILED"}
var available_gold := economy_service.get_balance(PLAYER_CASH_ACCOUNT) var available_gold := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
var remaining_gold := available_gold var remaining_gold := available_gold
var payments: Dictionary = {} var payments: Dictionary = {}
@@ -2592,6 +2626,17 @@ func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dicti
if not bool(upkeep_result[&"ok"]): if not bool(upkeep_result[&"ok"]):
return upkeep_result return upkeep_result
var upkeep_paid := int(upkeep_result[&"paid_total"]) var upkeep_paid := int(upkeep_result[&"paid_total"])
var rank_tax := _post_daily_finance_charge(day, boundary_minute, int(finance_due[&"rank_tax"]),
FinancePolicy.RANK_TAX_EXPENSE, FinancePolicy.RANK_TAX_PAYABLE,
FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY, "rank_tax")
if not bool(rank_tax[&"ok"]):
return rank_tax
var loan_interest := _post_daily_finance_charge(day, boundary_minute, int(finance_due[&"loan_interest"]),
FinancePolicy.LOAN_INTEREST_EXPENSE, FinancePolicy.LOAN_INTEREST_PAYABLE,
FinancePolicy.LOAN_INTEREST_CATEGORY, FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY, "loan_interest")
if not bool(loan_interest[&"ok"]):
return loan_interest
var finance_paid := int(rank_tax[&"paid"]) + int(loan_interest[&"paid"])
var financials := _build_operations_daily_financials( var financials := _build_operations_daily_financials(
int(payload[&"period_start_minute"]), int(payload[&"period_start_minute"]),
int(payload[&"period_end_minute_exclusive"]), int(payload[&"period_end_minute_exclusive"]),
@@ -2603,14 +2648,19 @@ func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dicti
var arrears_total := int(settlement.details.get(&"unpaid_after_total", -1)) var arrears_total := int(settlement.details.get(&"unpaid_after_total", -1))
if arrears_total < 0 \ if arrears_total < 0 \
or arrears_total != _staff_payroll_arrears_total(staff_service) \ or arrears_total != _staff_payroll_arrears_total(staff_service) \
or economy_service.get_balance(PLAYER_CASH_ACCOUNT) != available_gold - expected_paid_total - upkeep_paid \ or economy_service.get_balance(PLAYER_CASH_ACCOUNT) != available_gold - expected_paid_total - upkeep_paid - finance_paid \
or int(_state.get_resource(&"gold")) != economy_service.get_balance(PLAYER_CASH_ACCOUNT) \ or int(_state.get_resource(&"gold")) != economy_service.get_balance(PLAYER_CASH_ACCOUNT) \
or not bool(economy_service.validate_ledger().get(&"ok", false)) \ or not bool(economy_service.validate_ledger().get(&"ok", false)) \
or not bool(staff_service.validate_invariants().get(&"ok", false)): or not bool(staff_service.validate_invariants().get(&"ok", false)):
return {&"ok": false, &"reason": &"DAILY_CLOSE_POSTCONDITION_FAILED"} return {&"ok": false, &"reason": &"DAILY_CLOSE_POSTCONDITION_FAILED"}
var funds := _daily_fund_report(economy_service, day, int(payload[&"period_start_minute"]), boundary_minute)
if funds.is_empty():
return {&"ok": false, &"reason": &"DAILY_FUND_REPORT_FAILED"}
var settlement_report := settlement.details.duplicate(true) var settlement_report := settlement.details.duplicate(true)
settlement_report[&"upkeep_arrears_total"] = upkeep_settlement.get_arrears_total() settlement_report[&"upkeep_arrears_total"] = upkeep_settlement.get_arrears_total()
settlement_report[&"arrears_total"] = arrears_total + int(settlement_report[&"upkeep_arrears_total"]) settlement_report[&"funds"] = funds
settlement_report[&"arrears_total"] = arrears_total + int(settlement_report[&"upkeep_arrears_total"]) \
+ int(funds[&"rank_tax_arrears"]) + int(funds[&"loan_interest_arrears"])
return { return {
&"ok": true, &"ok": true,
&"category_totals": financials[&"category_totals"], &"category_totals": financials[&"category_totals"],
@@ -2647,6 +2697,8 @@ func _build_operations_daily_financials(
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION", &"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION", &"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", &"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT",
FinancePolicy.TOP_UP_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY,
FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY,
] ]
var category_mapping := { var category_mapping := {
&"SERVICE_SALE": &"revenue", &"SERVICE_SALE": &"revenue",
@@ -2660,6 +2712,8 @@ func _build_operations_daily_financials(
&"EVENT_COST_COMMITTED": &"cost_of_goods", &"EVENT_COST_COMMITTED": &"cost_of_goods",
&"DEFENSE_SKILL_COST": &"cost_of_goods", &"DEFENSE_SKILL_COST": &"cost_of_goods",
&"MAINTENANCE_ACCRUAL": &"maintenance", &"MAINTENANCE_ACCRUAL": &"maintenance",
FinancePolicy.RANK_TAX_CATEGORY: &"taxes_and_interest",
FinancePolicy.LOAN_INTEREST_CATEGORY: &"taxes_and_interest",
&"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows", &"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows",
&"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows", &"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows",
} }
@@ -3551,8 +3605,32 @@ func _operations_payroll_crosslinks_are_valid(
summary_settlement.erase(&"upkeep_arrears_total") summary_settlement.erase(&"upkeep_arrears_total")
elif upkeep_debt != 0: elif upkeep_debt != 0:
return false return false
var finance_arrears := 0
# A saved report is checked by the definition it was written with: without
# the mark it is version 1; any other mark is not a report this game wrote.
var fund_version := FUND_REPORT_VERSION
if summary_settlement.get(&"funds", null) is Dictionary:
var saved_funds: Dictionary = summary_settlement[&"funds"]
var mark: Variant = saved_funds.get(&"fund_report_version", saved_funds.get("fund_report_version", null))
if mark == null:
fund_version = 1
elif typeof(mark) != TYPE_INT or int(mark) != FUND_REPORT_VERSION:
return false
var expected_funds := _daily_fund_report(economy, day, coordinator.get_financial_period_start(day),
coordinator.get_daily_close_minute(day), fund_version)
if expected_funds.is_empty():
return false
if summary_settlement.has(&"funds"):
if not _strict_variants_equal(_operations_snapshot_comparison_value(summary_settlement[&"funds"]),
_operations_snapshot_comparison_value(expected_funds)):
return false
finance_arrears = int(expected_funds[&"rank_tax_arrears"]) + int(expected_funds[&"loan_interest_arrears"])
summary_settlement.erase(&"funds")
elif bool(expected_funds[&"has_finance_postings"]):
# Days closed before the funds were split carry no fund report and no postings.
return false
if int(summary_settlement.get(&"arrears_total", -1)) \ if int(summary_settlement.get(&"arrears_total", -1)) \
!= int(staff_record.get(&"unpaid_after_total", -2)) + upkeep_debt \ != int(staff_record.get(&"unpaid_after_total", -2)) + upkeep_debt + finance_arrears \
or summary_settlement.get(&"replayed", null) != false: or summary_settlement.get(&"replayed", null) != false:
return false return false
summary_settlement.erase(&"arrears_total") summary_settlement.erase(&"arrears_total")
@@ -3599,7 +3677,10 @@ func get_loan_principal_report() -> Dictionary:
loans.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"loan_id"]) < String(right[&"loan_id"])) loans.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"loan_id"]) < String(right[&"loan_id"]))
var valid: bool = _state.get_resource(&"gold") == cash and (loans.is_empty() or _loan_principal_ledger_is_valid(economy_service)) var valid: bool = _state.get_resource(&"gold") == cash and (loans.is_empty() or _loan_principal_ledger_is_valid(economy_service))
var ended: bool = _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal() var ended: bool = _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal()
return {&"available": true, &"ledger_valid": valid, &"terminal": ended, &"cash": cash, &"next_command_id": _next_command_id, &"loans": loans} # A09: a repayment may use the hidden fund for what the front fund (cash) cannot pay.
return {&"available": true, &"ledger_valid": valid, &"terminal": ended, &"cash": cash,
&"back_cash": economy_service.get_balance(FinancePolicy.BACK_CASH), &"available_funds": FinancePolicy.available_funds(economy_service),
&"next_command_id": _next_command_id, &"loans": loans}
## Atomic principal payment against an already attested loan liability. ## Atomic principal payment against an already attested loan liability.
@@ -3626,15 +3707,20 @@ func repay_loan_principal(
return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "The loan liability does not exist.") return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "The loan liability does not exist.")
var remaining := -economy_service.get_balance(liability) var remaining := -economy_service.get_balance(liability)
var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT) var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
if amount > remaining or amount > cash or _state.get_resource(&"gold") != cash: if amount > remaining or amount > FinancePolicy.available_funds(economy_service) or _state.get_resource(&"gold") != cash:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Principal payment exceeds the outstanding principal or available Gold, or its mirror is inconsistent.") return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Principal payment exceeds the outstanding principal or available Gold, or its mirror is inconsistent.")
# The single balanced posting is atomic. Defer receipts until the Gold # A09: what the front fund cannot pay comes from the hidden fund in the same
# mirror is synchronized, also respecting any enclosing effect scope. # balanced posting, so the payment and its top-up are atomic. Defer receipts
# until the Gold mirror is synchronized, also respecting any enclosing effect scope.
var minute := _runtime_event_minute()
var entries: Array = []
if amount > cash:
entries.append(LedgerEntry.new().configure(&"", minute, PLAYER_CASH_ACCOUNT, FinancePolicy.BACK_CASH,
amount - cash, FinancePolicy.TOP_UP_CATEGORY, loan_id, &"", &"finance.back_fund.top_up"))
entries.append(LedgerEntry.new().configure(&"", minute, liability, PLAYER_CASH_ACCOUNT,
amount, &"LOAN_PRINCIPAL_REPAID", loan_id))
economy_service.begin_notification_scope() economy_service.begin_notification_scope()
var result := economy_service.transfer( var result := economy_service.post(entries, idempotency_key)
PLAYER_CASH_ACCOUNT, liability, amount, _runtime_event_minute(),
&"LOAN_PRINCIPAL_REPAID", loan_id, idempotency_key
)
if result.is_success(): if result.is_success():
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT)) _state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
result.details[&"loan_id"] = loan_id result.details[&"loan_id"] = loan_id
@@ -3644,6 +3730,144 @@ func repay_loan_principal(
return result return result
## A09: what the front fund cannot pay is covered from the hidden fund first. The move
## is posted on its own (BACK_FUND_TOP_UP) and is neither income nor expense.
func top_up_front_fund(due: int, minute: int, idempotency_key: StringName) -> CommandResult:
var front := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
var amount := mini(maxi(due - front, 0), economy_service.get_balance(FinancePolicy.BACK_CASH))
if amount <= 0:
return CommandResult.succeeded(0, {&"amount": 0})
var result := economy_service.transfer(FinancePolicy.BACK_CASH, PLAYER_CASH_ACCOUNT, amount, minute,
FinancePolicy.TOP_UP_CATEGORY, &"", idempotency_key, &"finance.back_fund.top_up", &"", false)
# Callers publish the notification, which also refreshes the Gold projection.
if result.is_success():
result.details[&"amount"] = amount
return result
## Whole Gold the closed upkeep periods through the minute would owe, arrears
## included; -1 if a period cannot be planned. Nothing is changed.
func _upkeep_debt_through(minute: int) -> int:
var periods: Array = []
for period: Dictionary in facility_upkeep.get_closed_periods_from(upkeep_settlement.get_settled_period_count()):
if int(period[&"period_end"]) <= minute:
periods.append(period)
return upkeep_settlement.preview_debt(periods)
## A10/A11: the closed day's rank tax and loan interest, with their older arrears.
func _daily_finance_due(economy: EconomyService, period_start: int, boundary_minute: int) -> Dictionary:
var totals: Variant = _operations_expected_category_totals(economy, period_start, boundary_minute, 0)
if totals == null:
return {&"ok": false, &"reason": &"DAILY_SALES_UNAVAILABLE"}
var tax := FinancePolicy.rank_tax(int(totals[&"revenue"]))
var interest := FinancePolicy.loan_interest(FinancePolicy.outstanding_principal(economy))
return {&"ok": true, &"rank_tax": tax, &"loan_interest": interest,
&"due_total": tax + interest - economy.get_balance(FinancePolicy.RANK_TAX_PAYABLE)
- economy.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE)}
## Accrues a daily charge at the end of the closed day and pays its older arrears
## first, then the new amount, from the front fund; the rest stays payable.
func _post_daily_finance_charge(day: int, boundary_minute: int, new_amount: int, expense: StringName,
payable: StringName, category: StringName, payment_category: StringName, kind: String) -> Dictionary:
var due := new_amount - economy_service.get_balance(payable)
var paid := mini(due, economy_service.get_balance(PLAYER_CASH_ACCOUNT))
var source := StringName("finance-day-%08d" % day)
var entries: Array = []
if new_amount > 0:
entries.append(LedgerEntry.new().configure(&"", boundary_minute - 1, expense, payable, new_amount,
category, source, &"", StringName("finance.%s.accrued" % kind)))
if paid > 0:
entries.append(LedgerEntry.new().configure(&"", boundary_minute, payable, PLAYER_CASH_ACCOUNT, paid,
payment_category, source, &"", StringName("finance.%s.paid" % kind)))
if not entries.is_empty():
var posted := economy_service.post(entries, StringName("finance.%s.day.%08d" % [kind, day]), &"", false)
if not posted.is_success():
return {&"ok": false, &"reason": &"FINANCE_LEDGER_POST_FAILED", &"result": posted.to_dictionary()}
if not _stage_operations_economy_notification(posted):
return {&"ok": false, &"reason": &"FINANCE_NOTIFICATION_STAGE_FAILED"}
return {&"ok": true, &"new": new_amount, &"paid": paid, &"arrears": due - paid}
## The fund side of a closed day, derived only from the ledger, so the live close
## and a restored save produce the same report. Version 2 (marked in the report):
## back_top_up is every top-up of the business day, the operation-time purchases
## and repayments and the legacy 00:00 upkeep as well as the close's own. A report
## without the mark is version 1, written while back_top_up counted only the
## close's own top-up; a restore checks it by that definition, not a looser one.
func _daily_fund_report(economy: EconomyService, day: int, period_start: int, boundary_minute: int,
version := FUND_REPORT_VERSION) -> Dictionary:
if economy == null or day <= 0 or period_start < 0 or boundary_minute <= period_start \
or version not in [1, FUND_REPORT_VERSION]:
return {}
var report := {
&"revenue_front": 0, &"revenue_back": 0, &"back_top_up": 0,
&"rank_tax": 0, &"rank_tax_paid": 0, &"loan_interest": 0, &"loan_interest_paid": 0,
}
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute >= period_start and entry.absolute_minute < boundary_minute \
and entry.category == &"SERVICE_CASH_SWEEP":
if entry.debit_account == FinancePolicy.BACK_CASH:
report[&"revenue_back"] = int(report[&"revenue_back"]) + entry.amount
elif entry.debit_account == PLAYER_CASH_ACCOUNT:
report[&"revenue_front"] = int(report[&"revenue_front"]) + entry.amount
var keyed := {
&"back_top_up": StringName("finance.back_top_up.day.%08d" % day),
&"rank_tax": StringName("finance.rank_tax.day.%08d" % day),
&"loan_interest": StringName("finance.loan_interest.day.%08d" % day),
}
var postings := false
for field: StringName in keyed:
var transaction := economy.get_transaction_id_for_idempotency_key(keyed[field])
if transaction.is_empty():
continue
postings = true
for entry: LedgerEntry in economy.get_transaction_entries(transaction):
if field == &"back_top_up" and entry.category == FinancePolicy.TOP_UP_CATEGORY:
report[field] = int(report[field]) + entry.amount
elif field != &"back_top_up" and entry.category in [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]:
report[field] = int(report[field]) + entry.amount
elif field != &"back_top_up" and entry.category in [FinancePolicy.RANK_TAX_PAYMENT_CATEGORY, FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY]:
report[StringName(String(field) + "_paid")] = int(report[StringName(String(field) + "_paid")]) + entry.amount
else:
return {}
if version == FUND_REPORT_VERSION:
# The closes are keyed; the previous day's close is posted at period_start.
var closes := {}
for close_day: int in [day - 1, day]:
var close := economy.get_transaction_id_for_idempotency_key(StringName("finance.back_top_up.day.%08d" % close_day))
if not close.is_empty():
closes[close] = true
for entry: LedgerEntry in economy.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY and entry.absolute_minute >= period_start \
and entry.absolute_minute < boundary_minute and not closes.has(entry.transaction_id):
report[&"back_top_up"] = int(report[&"back_top_up"]) + entry.amount
postings = true
report[&"fund_report_version"] = FUND_REPORT_VERSION
report[&"rank_tax_arrears"] = _payable_arrears_through(economy, FinancePolicy.RANK_TAX_PAYABLE, boundary_minute)
report[&"loan_interest_arrears"] = _payable_arrears_through(economy, FinancePolicy.LOAN_INTEREST_PAYABLE, boundary_minute)
report[&"has_finance_postings"] = postings
return report
func _payable_arrears_through(economy: EconomyService, payable: StringName, minute: int) -> int:
var arrears := 0
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute > minute:
continue
if entry.credit_account == payable:
arrears += entry.amount
elif entry.debit_account == payable:
arrears -= entry.amount
return arrears
func _finance_arrears_through(economy: EconomyService, minute: int) -> int:
return _payable_arrears_through(economy, FinancePolicy.RANK_TAX_PAYABLE, minute) \
+ _payable_arrears_through(economy, FinancePolicy.LOAN_INTEREST_PAYABLE, minute)
func _loan_principal_ledger_is_valid(economy: EconomyService) -> bool: func _loan_principal_ledger_is_valid(economy: EconomyService) -> bool:
if economy == null: if economy == null:
return false return false
@@ -3697,6 +3921,8 @@ func _operations_expected_category_totals(
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION", &"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION", &"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", &"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT",
FinancePolicy.TOP_UP_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY,
FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY,
] ]
var mapping := { var mapping := {
&"SERVICE_SALE": &"revenue", &"REFUND": &"refunds", &"SERVICE_SALE": &"revenue", &"REFUND": &"refunds",
@@ -3707,6 +3933,8 @@ func _operations_expected_category_totals(
&"EVENT_COST_COMMITTED": &"cost_of_goods", &"EVENT_COST_COMMITTED": &"cost_of_goods",
&"DEFENSE_SKILL_COST": &"cost_of_goods", &"DEFENSE_SKILL_COST": &"cost_of_goods",
&"MAINTENANCE_ACCRUAL": &"maintenance", &"MAINTENANCE_ACCRUAL": &"maintenance",
FinancePolicy.RANK_TAX_CATEGORY: &"taxes_and_interest",
FinancePolicy.LOAN_INTEREST_CATEGORY: &"taxes_and_interest",
&"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows", &"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows",
&"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows", &"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows",
} }
@@ -5254,6 +5482,7 @@ func _initialize_economy() -> void:
facility_upkeep = FacilityUpkeepAccrual.new() facility_upkeep = FacilityUpkeepAccrual.new()
upkeep_settlement = FacilityUpkeepSettlement.new() upkeep_settlement = FacilityUpkeepSettlement.new()
FacilityUpkeepSettlement.prepare_accounts(economy_service) FacilityUpkeepSettlement.prepare_accounts(economy_service)
FinancePolicy.prepare_accounts(economy_service)
facility_upkeep.start_at(_clock.absolute_minute) facility_upkeep.start_at(_clock.absolute_minute)
mana_reserve = ManaReserveCycle.new() mana_reserve = ManaReserveCycle.new()
mana_reserve.start_at(_clock.absolute_minute) mana_reserve.start_at(_clock.absolute_minute)
@@ -11960,6 +12189,12 @@ func _apply_service_transaction(
if service.price_snapshot > 0: if service.price_snapshot > 0:
var payer_account := _guest_wallet_account(request.guest_ids[0]) var payer_account := _guest_wallet_account(request.guest_ids[0])
var revenue_account := _facility_revenue_account(facility.instance_id) var revenue_account := _facility_revenue_account(facility.instance_id)
# Only a registered definition with a hidden-service tag routes the sale to the
# hidden fund. A facility whose definition cannot be read keeps the existing
# front-fund behaviour (disclosed in execution/p05-02/REPAIR_02.md).
var sale_definition := definition_registry.get_facility(facility.definition_id)
var sale_fund := FinancePolicy.BACK_CASH \
if sale_definition != null and FinancePolicy.is_back_fund_service(sale_definition.tags) else PLAYER_CASH_ACCOUNT
if not economy_service.has_account(payer_account) or not economy_service.has_account(revenue_account): if not economy_service.has_account(payer_account) or not economy_service.has_account(revenue_account):
inventory_service.restore_snapshot(inventory_before) inventory_service.restore_snapshot(inventory_before)
return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "A service settlement account is missing.") return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "A service settlement account is missing.")
@@ -11969,7 +12204,7 @@ func _apply_service_transaction(
service.price_snapshot, &"SERVICE_SALE", facility.instance_id service.price_snapshot, &"SERVICE_SALE", facility.instance_id
), ),
LedgerEntry.new().configure( LedgerEntry.new().configure(
&"", absolute_minute, PLAYER_CASH_ACCOUNT, revenue_account, &"", absolute_minute, sale_fund, revenue_account,
service.price_snapshot, &"SERVICE_CASH_SWEEP", facility.instance_id service.price_snapshot, &"SERVICE_CASH_SWEEP", facility.instance_id
), ),
] ]
@@ -3925,6 +3925,39 @@
"text": "\tprint(\"%s: %d checks\" % [\"PASS\" if _failures == 0 else \"FAIL\", _checks])" "text": "\tprint(\"%s: %d checks\" % [\"PASS\" if _failures == 0 else \"FAIL\", _checks])"
} }
] ]
},
{
"test": "tests/economy/test_p05_funds_runtime.gd",
"template": "PASS: <checks> checks",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "4c8f929b0de76198182dfaaef4be4bea7961faa9c31c01889497d6675e0c5def",
"authority": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
"scope": "new P05-02 suite marker; native execution pending"
}
},
{
"test": "tests/economy/test_p05_funds_restart_1_write.gd",
"template": "PASS: <checks> checks",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "3b28e94f1a1f577d74c5322d532b32854339dd4731532ac0b21b4fbc7ffb12f3",
"authority": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
"scope": "new P05-02 F6 restart writer marker; native execution pending"
}
},
{
"test": "tests/economy/test_p05_funds_restart_2_read.gd",
"template": "PASS: <checks> checks",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "1b5bbf0f5c58df39ec28cc867c18a1a14d293fd36459343a118a3e97bc31d33a",
"authority": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
"scope": "new P05-02 F6 restart reader marker; native execution pending"
}
} }
], ],
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting", "unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
@@ -0,0 +1,19 @@
{
"schema": "p05-02-expectation-changes-v1",
"rule": "SPEC §4 F8: an existing test expectation changes only where the difference is explained by A09, A10, A11 or A12 alone",
"changes": [
{
"test": "tests/business/test_required_kitchen.gd",
"function": "_approved_runtime_migrations",
"decision": "A12",
"before": "a legacy v14 save loaded back equals the written state plus the approved mana_reserve and elevators migrations",
"after": "the same, plus the five empty fund accounts A12 creates on load (BACK_CASH and RANK_TAX_EXPENSE and LOAN_INTEREST_EXPENSE non-negative; RANK_TAX_PAYABLE and LOAN_INTEREST_PAYABLE allowing negative; none system); every other economy field must stay byte-identical (checked)",
"results_local_diagnostic": {
"base_dda3eed": "163 checks, 0 failures",
"draft_before_change": "163 checks, 2 failures (legacy migration control; whole typed loaded state)",
"draft_after_change": "163 checks, 0 failures"
}
}
],
"not_changed_by_this_unit": "the other suites that end nonzero in a direct headless run (Main/audio teardown asks for the guarded runner proof; the export pack test needs a built pack; tests/s0/test_kernel_and_save.gd times out at 600 s) end the same way on the unchanged base dda3eed"
}
@@ -0,0 +1,18 @@
# P05-02 の F6(別の process での再開)と F7(通常の操作で開く帳簿)の作り
SPEC(`SPEC.md` §6)の roster 8本は変えない。F6 は SPEC §6 のとおり `tools/run_p05_funds_tests.py` と `tests/**` の範囲で、書き手と読み手の2つの段として roster の後に足した。F7 は SPEC §6 のとおり新しい suite(`tests/economy/test_p05_funds_runtime.gd`)の中で確かめ、帳簿を Main の通常の操作で開く形にした。
## F6: 別の process での再開
- 書き手 `tests/economy/test_p05_funds_restart_1_write.gd` と読み手 `tests/economy/test_p05_funds_restart_2_read.gd`。共通の fixture と日の筋書きは `tests/helpers/p05_funds_restart_fixture.gd`(新しい suite と同じ帳簿の fixture と 30 分ごとの境界の進め方)。
- 日の筋書き: 1日目に借入 1000・裏の売上 700(税 70・利息 10)。2日目に経費で表金が 5 になり、利息 10 のうち 5 を裏金から補填(A09)。3日目に表の売上 50(税 5・利息 10)。最後は表金 35・裏金 695。
- 書き手は、保存しない通しの実行と、2日目の途中(2400)・2日目の 00:00 の締めの直前(2850)・直後(2880)に通常のセーブファイル(`save_to_path`)を書く実行をする。セーブの包み(`manifest.json`・`world.bin`)の bytes を、通しの実行の帳簿の SHA-256 と日次の締めの値と一緒に、宣言した生成ファイル `res://artifacts/p05-02/restart-handoff.json` に書き、`P05_RESTART_WRITER pid=<書き手の PID> handoff_sha256=<SHA>` を1行だけ出す。
- 読み手は次の段の別の engine の process で、handoff を読み(書き手の PID が自分と違うこと、書き手の source の SHA が今のものと同じことを確かめる)、`P05_RESTART_READER pid=<読み手の PID> writer_pid=<書き手の PID> handoff_sha256=<SHA>` を1行だけ出す。自分の profile に包みの bytes をそのまま置き(SHA を確かめる)、新しい Runtime で `load_from_path` で読み、残りの筋書きを進める。3つの保存点のどれから再開しても、帳簿の bytes が通しの実行(自分の process と書き手の process の両方)と同じで、日次の締め・資金の報告・滞納が同じで、税と利息の計上が5回、補填が1回だけ(二重にならない)であることを確かめる。`world.bin` を1 byte 変えた包みは読込を拒否され、Gold も時刻も変わらない。
- 段をまたいで bytes を保つ共有の仕組みは、`tools/regression/suite-policy.json` の `fixed_generated_report_routes` だけである(共有の `declared_reports` が、後の全ての段の前に、それまでの生成ファイルの bytes の一致を確かめる)。そこで書き手の1つだけの生成ファイルとして handoff を宣言した。狭い roster の8本には生成ファイルはなく、`tests/mvp/test_runtime_upkeep.gd` を roster に入れない扱い(SPEC §6)は変えていない。
- `tools/run_p05_funds_tests.py` の照合で足したこと: 段は import・parse・roster 8・再開 2 の 12。書き手の段の生成ファイルは handoff の1つだけで、残した stage の bytes・書き手の PID・書き手の source の SHA と一致する。書き手が出した行の PID と SHA、handoff に書かれた PID が書き手の段の実際の PID と一致する。読み手が出した行の PID が読み手の段の実際の PID、書き手の PID が書き手の段の実際の PID と一致し(両者は違う)、SHA が handoff と一致する。他の段は生成ファイルを持たない。残した stage は source と handoff だけ。外の報告の経路(`outside_routes`)は使わない。
- 全体の roster(`tools/run_all_tests.py`)は名前の順に走るので、書き手が読み手より先に走る(`tools/run_p05_funds_tests.py` が起動の前に確かめる)。全体の照合(`tools/regression/review_protocol.py`)は宣言した生成ファイルを同じ規則で確かめる。読み手だけを単独で直接走らせると、handoff がないので失敗する(書き手を先に走らせる必要がある)。
## F7: 通常の操作で開く帳簿
- 新しい suite の最後に、ゲームの本物の `SimulationRuntime`・`GameClock`・`GameState` と `scenes/main/main.tscn` を使い、1日目に借入 1000・表の売上 300・裏の売上 500 を置いて、通常の時計で 00:00 の締めまで進める。Main の左の「帳簿」のボタン(`navigation_buttons[&"ledger"]`)を押して帳簿を開き、表金 6210・裏金 500、本日(00:00 に始まる2日目)の税 80・利息 10 の支払い、1日目の締めの「うち表の売上 300G / 裏の売上 500G」「世界樹ランク税 80G(支払 80G)/ 利息 10G(支払 10G)」「裏金から表金へ補填 0G / 未払い 税 0G・利息 0G」を読む。
- Main を使うので、新しい suite の方針を通常の Main の音声の後始末の証明つき(`ordinary_cleanup_prepared`、札 `single`)にした(`tests/ui/test_upkeep_closed_ui.gd` と同じ)。最後に `tests/helpers/ui_audio_fixture_cleanup.gd` で Main を片付け、その証明を guarded の runner が照合する。
@@ -0,0 +1,51 @@
# P05-02 正式受入記録
P05-02「全費目と資金区分」を、受入 8 件(F1〜F8)の別の fresh な Desktop Opus 5.5 の独立レビューの PASS(高・中 0、low 3)、通常の validation・review・integration・final の全 PASS、実 DB と traceability の結合で正式に完了した。S 8954914 → Q cb183fd。正式 24/88・4/30、部分点なし。P05 パッケージ(P05-03)、通常の操作での裏金(原子要件 back_gold)、P09・P11 の残り、UI の low 2 件、Windows・全ゲームの受入は未達のまま。
提出 S: `895491456477b42a74e59fa239bea86c4fb9aa33`(修正 3)
統合・最終検証 Q: `cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83`(Q の tree は S の tree と同じ `46cdbfa7…`)
基準 M: `613b9886dae31b2578f6aa13b81712b68ac694bf`
正式証拠: `P05_02_FORMAL_COMPLETION_CB183FD.json`
独立レビューの結果(実結果の bytes の写し): `P05_02_INDEPENDENT_REVIEW_8954914.json` SHA256 `fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5`
## 原本の条件
- 原 work: 表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税を帳簿へ接続する。
- 原 acceptance: 3営業日で資金増減と台帳が一致し、借入元本を売上に含めない。
- 横断 6 条件(P05): 原本条件とUI操作が一致、Runtimeの実利用・会計・行動へ接続、途中保存・再起動で再現、異常/境界/中断を検証、関連回帰PASS、要件別ログとソースハッシュを保存。
- 受入 8 件(F1〜F8)は登録の `acceptance_baseline.json` の本文のとおり(証明の `acceptance_ids`)。決定 A09〜A12 の出典は `artifacts/original-completion-2026-10-02/feature-priority-r01/DECISIONS_P05_02.json`。
## 4つの通常のゲート(Runner の the-tree-p05-02、task P05-02、completed 8/8)
| ゲート | commit | Runner の証跡 | 結果 |
|---|---|---|---|
| validation | S | evidence 6 | p05-funds exit 0、verified true、10 段・867 checks、engines_started 0 |
| review | S | evidence 7 | 実結果 `fb3e5be2…`、pass、F1〜F8、low 3 |
| integration | Q | evidence 8 | p05-funds exit 0、verified true、engines_started 0 |
| final | Q | evidence 9 | `ec32bfcd…-final.json` SHA `f8d25e28…`、verified true、engines_started 0 |
- owner の承認: Root が、The Tree の main の統合と push についての既存の人の許可を、正確な Q `cb183fd` と main_base `613b988` に正直に当てはめた(新しい人の発言は作っていない)。Runner の approvals の行と `ROOT_OWNER_APPROVAL_CB183FD_R01.json`。integrate の job `2509b410d93e49d998624da6bb163053`。
- 測定は全て S で行った(native 10 段・867 checks、最終の S の全 roster 195 本・22,539 checks)。Q での integration と final は、S で測った証拠を、中身が同じ Q の新しい checkout で中身ベースの verifier が照合したもので、Q で測ったとは書かない。
## 独立レビュー
- 別の fresh な Claude Desktop Opus 5.5 の session(実装担当と別。前の S の 2 回のレビューとも別)。凍結の入力(packet・schema・material 7,055 ファイル)の SHA を確かめ、読むだけで判定した。会話の記録の全体を配送の前に確かめた(Bash 80 件は全て読むだけ。結果は `open(..., 'xb')` で 1 回だけ書いた。engine・試験・Runner のコマンド・git の書き換えは無い)。
- low 3 件(原文は独立レビューの結果のファイル):
- 仕入れの画面(allowed_paths の外)が「手持ち」に表金だけ、「購入後」に表金+裏金の残りを出す。操作は A09 どおり。後の作業として開示(ユーザーの機能を先にする指示)。
- 帳簿の画面の資金の欄が表金を「手持ち」と出し、借入が無い時は表金か合計かが読み取りにくい。後の作業として開示(UI は後で入れ替える予定)。
- 修正 2・3 の局所の確認の生ログが scratchpad にだけあった。→ 32 件を管理用のフォルダへ写し、記録の SHA・bytes と一致を確かめた(`P05_02_LOCAL_CHECK_RAW_LOGS_RETAINED_R01.json`)。Root も 32 件の元と写しを読み直し、違いは無かった。
## 保持した失敗
- S `7808ca7` の validation の失敗、S `37deb6e`(r02)と S `d65e3da`(r01)のレビューの changes_requested、修正 2・3 の局所の確認と試作の失敗。どれも元の記録のまま残し、新しい S の証拠に流用していない。
## 未達として残すもの
- P05 パッケージは in_progress のまま。P05-03「金庫と銀行の通常営業」は planned で、人への問い 3 件は回答待ち(推定しない)。
- 通常の操作での裏金(原子要件 back_gold): F1 の裏の施設の場面は、通常の Main では雇えない役割(service_attendant)を register_staff の command で登録した。P09・P11 の原の残件(remaining_paid_role、2026-10-08)で、ここでは閉じない。
- カジノ・脅迫金・ユニット売却の売上の発生源は無い(SPEC §7 の開示)。罰金・出来事は表金だけで払う(A09 の範囲。専用の試験は無いが開示済み)。
- 退職金(AR.staff.dismissal_severance、P05、D04)はこの単位に入らない。人の回答待ち。
- 同一の Windows 配布版の実機での受入、長い通常のプレイ、全ゲームの最終受入。
実装・native・全 roster は登録の実装担当(Claude Opus 5.5 Desktop)が行い、commit は Runner の submit が作った。契約の worker 欄の codex は互換のためだけで、Codex・Astra の run や dispatch は無い。原 88 の分母は増やさない。
@@ -0,0 +1,526 @@
{
"at_jst": "2026-10-23T22:50:18.074400+09:00",
"original_unit": "P05-02",
"project": "the-tree-p05-02",
"task": "P05-02",
"state": "completed",
"S": "895491456477b42a74e59fa239bea86c4fb9aa33",
"Q": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"M": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"repairs": 3,
"Q_tree_equals_S_tree": "46cdbfa7629384fa53a83196efff274abb0dfd9b",
"all4_normal_gates_passed": true,
"original_work": "表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税を帳簿へ接続する。",
"original_acceptance": "3営業日で資金増減と台帳が一致し、借入元本を売上に含めない。",
"original_package_work": "表/裏収入区分、維持費、返済元本/利息、税、未払いを帳簿に接続。金庫上限・購買・銀行のサービス契約を先に作る。決算損益と現金収支を区別。",
"original_package_acceptance": "3営業日の費目別総額が台帳・施設別・資金と一致。借入元本を売上にしない。支払い失敗/再送/復元/3赤字を検証。",
"original_cross_cutting_acceptance": [
"原本条件とUI操作が一致",
"Runtimeの実利用・会計・行動へ接続",
"途中保存・再起動で再現",
"異常/境界/中断を検証",
"関連回帰PASS",
"要件別ログとソースハッシュを保存"
],
"acceptance_ids": [
{
"id": "P05-02-F1",
"text": "表金(PLAYER_CASH)と裏金(BACK_CASH)が別の口座で、施設の定義に restricted_service か adult_only の札がある施設の売上だけが裏金に入り、それ以外(飲食・宿泊など)は表金に入る。表・裏の施設を各1つ以上で確かめる。"
},
{
"id": "P05-02-F2",
"text": "A09: 00:00 の精算(維持費・人件費・税・利息)は表金から先に払い、足りない分を裏金で補い、両方で足りない分を滞納として翌日へ繰り越す。操作の時の支払(仕入れの手動・自動、借入の返済)は表金+裏金が足りれば表金から先に払い、足りなければ拒否して何も変えない。対象外の支払(建設・罰金など)は今までどおり表金だけ。境界: 表金だけで足りる、裏金で補う、両方で足りず滞納、滞納の翌日の支払。"
},
{
"id": "P05-02-F3",
"text": "A10: 第 d 日の 00:00 に、税 = floor(第 d 日の売上(表+裏)× 10 / 100) を RANK_TAX として計上し、決算の taxes_and_interest に入れる。0 は記帳しない。第 d+1 日に二重に数えない。不足は滞納として繰り越し、次の精算で新しい額より先に払う。"
},
{
"id": "P05-02-F4",
"text": "A11: 毎日 00:00 に、利息 = floor(借入残高 × 1 / 100) を LOAN_INTEREST として計上し、決算の taxes_and_interest に入れる。借入・返済の後の残高で正しく変わる。借入の元本は売上にも経費にも入らない。"
},
{
"id": "P05-02-F5",
"text": "3営業日の照合: 表金・裏金それぞれの日ごとの増減が帳簿の記帳の合計と一致し、日次決算の全ての区分(表裏の売上・仕入れ・人件費・維持費・税と利息・罰金など)が帳簿と一致し、借入の元本が売上に入らない。"
},
{
"id": "P05-02-F6",
"text": "途中保存・00:00 の前後の保存・別の process での再開で、税・利息・振り分け・支払が二重にならず、同じ結果になる。旧 snapshot(裏金の口座が無い版)は PLAYER_CASH の全額を表金、裏金 0 として読み(A12)、帳簿の履歴は変えない。不正な値は拒否し、読込の前の状態を保つ。"
},
{
"id": "P05-02-F7",
"text": "帳簿の画面と日次のレポートを通常の操作で開き、表金・裏金の残高、表裏の売上、税、利息、滞納が読める(UI は今の部品のまま。完成の見た目は求めない)。"
},
{
"id": "P05-02-F8",
"text": "関連の回帰と最終Sの全 roster が PASS。既存試験の期待値変更はA09・A10・A11の承認済み差、または§3.8のA12旧セーブ移行の差だけに限定し、全件を EXPECTATION_CHANGES.json に変更前後の値・計算または構造差・根拠つきで一覧化する。A12は旧セーブへの5口座(BACK_CASH、LOAN_INTEREST_EXPENSE、LOAN_INTEREST_PAYABLE、RANK_TAX_EXPENSE、RANK_TAX_PAYABLE)の追加だけとし、残高・既存口座・取引履歴・その他の帳簿フィールドの不変をbyte比較で確認する。元の失敗を保存し、未解決の試験を除外してPASSにしない。"
}
],
"acceptance_baseline": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/acceptance_baseline.json",
"bytes": 3987,
"sha256": "31d682e5cf57637edbfe4cc1d57dff23d646f98efa86001a61368303b0f74f2e"
},
"spec": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
"bytes": 17246,
"sha256": "3ba7af53d6a2b005cdf16b7957fbb7cc902339d9626dfb9dcd4d4c792f54e44f",
"contract_spec_sha256": "b3339ce4cbc5400a70962159f5c2c193b2132e76b7dcbb477dfbad4e0f51b6b2"
},
"decisions_A09_A12": {
"path": "artifacts/original-completion-2026-10-02/feature-priority-r01/DECISIONS_P05_02.json",
"bytes": 2135,
"sha256": "87828430f5f5a9c3ceed4916e56addc79c984a9aa2563e537d160c5fcfcaddc8"
},
"unit_disclosures": [
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/PROVENANCE.json",
"bytes": 6855,
"sha256": "adeab21581e539875169acbec1ee48feba814d0b1c445f6c5ab172d009257389"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/EXPECTATION_CHANGES.json",
"bytes": 1313,
"sha256": "6e6eb9e32d4cfaab86c163902b116bff98dffbb5af7cfbdfc633558e86eeec51"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/F6_F7_IMPLEMENTATION.md",
"bytes": 5380,
"sha256": "a578e0a60ac3e22946ce3b495e6d927310e2e14b2c00c77f17fe9b511fca64ae"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/REPAIR_01.md",
"bytes": 1340,
"sha256": "53a3beefb7d8c0e34f60bdc2f8fb38f354b464d33a07d16471e812d7a31b46db"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/REPAIR_02.md",
"bytes": 7948,
"sha256": "c656f4362260cf613ef8e193b65309fa0d075076d88f0b311361873e86a701ae"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/REPAIR_03.md",
"bytes": 5770,
"sha256": "7fd80675ccff48fa2018383e69646f2b8101b454cdc197133a3f42482824a9d1"
}
],
"runner_task": {
"id": "P05-02",
"state": "completed",
"submitted": "895491456477b42a74e59fa239bea86c4fb9aa33",
"candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"main_base": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"reason": null,
"repairs": 3
},
"runner_events_from_review": [
{
"id": 35,
"event": "review_pending",
"created": 1792762636.352181
},
{
"id": 36,
"event": "owner_actual_independent_Desktop_review_output",
"created": 1792762644.6540768
},
{
"id": 37,
"event": "reviewed",
"created": 1792762644.6552699
},
{
"id": 38,
"event": "approval_pending",
"created": 1792762742.227736
},
{
"id": 39,
"event": "approved",
"created": 1792762829.3107798
},
{
"id": 40,
"event": "completed",
"created": 1792762841.389877
}
],
"normal_evidence_join": [
{
"id": 6,
"task_id": "P05-02",
"kind": "validation",
"commit_sha": "895491456477b42a74e59fa239bea86c4fb9aa33",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/d661df3aacae40f7a32db1b5d0c6ea5d-validation.json",
"passed": 1,
"artifact_sha256": "363329adca9b3ba135d09004be3a8e371f336311db1e4798f457d97b6b08eae4",
"checks": [
{
"id": "p05-funds",
"exit_code": 0,
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/f6dee1bbc95849c6af9d40108c6bf045",
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p05-02/P05-02-validation-efe3b38b",
"stdout": {
"verified": true,
"actual_suites": 10,
"parse_checks": 345,
"actual_checks": 867,
"engines_started": 0,
"formal_acceptance": false,
"original_unit_complete": false
},
"raw": {
"command.json": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/f6dee1bbc95849c6af9d40108c6bf045/command.json",
"bytes": 719,
"sha256": "4e2ef2b7a79995d1277a24970e61013e9fcb64a9ca6a9b1febec37eb52d5a9e3"
},
"stdout.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/f6dee1bbc95849c6af9d40108c6bf045/stdout.log",
"bytes": 166,
"sha256": "c6cef78e638b60c51f9e014ecd374158cd6f138915678d73b7c874e845849844"
},
"stderr.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/f6dee1bbc95849c6af9d40108c6bf045/stderr.log",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"sandbox.sb": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/f6dee1bbc95849c6af9d40108c6bf045/sandbox.sb",
"bytes": 1920,
"sha256": "b6f48231243d4131c3030a122cf99d0bf19dcb93363f5dcd0d14e7f09d93213c"
}
}
}
]
},
{
"id": 7,
"task_id": "P05-02",
"kind": "review",
"commit_sha": "895491456477b42a74e59fa239bea86c4fb9aa33",
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/actual-independent-desktop-output/result.json",
"passed": 1,
"artifact_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
"checks": []
},
{
"id": 8,
"task_id": "P05-02",
"kind": "integration",
"commit_sha": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/8496f92e04474d4d814406e185964210-integration.json",
"passed": 1,
"artifact_sha256": "7174dd77cf513fa350d34c4aa861d4c22b054ca29b59a869a4f0ccfe44d43628",
"checks": [
{
"id": "p05-funds",
"exit_code": 0,
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/aab161fd1f65485c8efed780e27b1ecd",
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p05-02/P05-02-integration-77b0fddc",
"stdout": {
"verified": true,
"actual_suites": 10,
"parse_checks": 345,
"actual_checks": 867,
"engines_started": 0,
"formal_acceptance": false,
"original_unit_complete": false
},
"raw": {
"command.json": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/aab161fd1f65485c8efed780e27b1ecd/command.json",
"bytes": 720,
"sha256": "0ffbbd87dc9cbe3736b0b922685cc8028313df071cb07e77a4b6e4821e183041"
},
"stdout.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/aab161fd1f65485c8efed780e27b1ecd/stdout.log",
"bytes": 166,
"sha256": "c6cef78e638b60c51f9e014ecd374158cd6f138915678d73b7c874e845849844"
},
"stderr.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/aab161fd1f65485c8efed780e27b1ecd/stderr.log",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"sandbox.sb": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/aab161fd1f65485c8efed780e27b1ecd/sandbox.sb",
"bytes": 1921,
"sha256": "ed7a1f2c41212537bcdb79548f03d055b6637b6ad4bc1d1975cadbb4a15ebb83"
}
}
}
]
},
{
"id": 9,
"task_id": "P05-02",
"kind": "final",
"commit_sha": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/ec32bfcdd72e44bdaa43f5aa69dec5c1-final.json",
"passed": 1,
"artifact_sha256": "f8d25e287894735b87b6179a730e0db34c112200bd7f87cff1ca936a5a20ece0",
"checks": [
{
"id": "p05-funds",
"exit_code": 0,
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/6f7443a82356484997255ed18f06c951",
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p05-02/P05-02-final-9d2e7098",
"stdout": {
"verified": true,
"actual_suites": 10,
"parse_checks": 345,
"actual_checks": 867,
"engines_started": 0,
"formal_acceptance": false,
"original_unit_complete": false
},
"raw": {
"command.json": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/6f7443a82356484997255ed18f06c951/command.json",
"bytes": 726,
"sha256": "4eeb6b0523e20cbaa40ebcd8bc2a2e21ef5109a30b9f5bf01a606c1ccfb2f60c"
},
"stdout.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/6f7443a82356484997255ed18f06c951/stdout.log",
"bytes": 166,
"sha256": "c6cef78e638b60c51f9e014ecd374158cd6f138915678d73b7c874e845849844"
},
"stderr.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/6f7443a82356484997255ed18f06c951/stderr.log",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"sandbox.sb": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/runs/6f7443a82356484997255ed18f06c951/sandbox.sb",
"bytes": 1915,
"sha256": "fbdb8fbc586d379e664d05d3009173ee54498af477222eb767ef06a8c43fdc87"
}
}
}
]
}
],
"approval": {
"task_id": "P05-02",
"candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"main_base": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"owner": "Root applying existing human authorization for The Tree main integration and push, delegated continuation; exact candidate cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83 at main-base 613b9886dae31b2578f6aa13b81712b68ac694bf; no new human utterance claimed. Review S8954914 pass F1-F8 low3; diagnostic retention resolved, two UI lows disclosed and deferred per feature-first instruction.",
"created": 1792762829.309799
},
"owner_records": [
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/ROOT_OWNER_APPROVAL_CB183FD_R01.json",
"bytes": 1306,
"sha256": "475c9c3e59336f1de978c45c3b6f59af00d42743cae81de090313c8b85507c01"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/ROOT_P05_02_COMPLETED_DB_JOIN_R01.json",
"bytes": 3368,
"sha256": "5b163d1da07b1d6c4851aa8e41aa02db4a8a64bd2631d6c0839f019ffd88b882"
}
],
"integrate_job_id": "2509b410d93e49d998624da6bb163053",
"traceability": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p05-02/traceability/P05-02.json",
"bytes": 5158,
"sha256": "7a37ceb8c5eb58c61d884a80b44d3f6f6774baee8ff8c40cbf1de0236b55211d"
},
"independent_reviewer": {
"task_id": "P05-02",
"commit": "895491456477b42a74e59fa239bea86c4fb9aa33",
"session_id": "local_cbdd640f-21ad-4f21-8d71-12b8debccc21",
"model": "claude-opus-5-5",
"context": "fresh",
"read_only": true,
"read_only_meaning": "the governor read every tool call of the full reviewer transcript (792 rows, one user prompt carrying the brief SHA e5817d74..., no sidechain, model claude-opus-5-5 only): the 80 Bash commands are reads, SHA computations, git diff/log/show/status in the frozen worktree, read-only sqlite (mode=ro) of the Runner state and one temporary diff in the reviewer's own TMPDIR; Write was used only for the reviewer's own scratchpad draft; DESKTOP_RESULT.json was written once with open(..., 'xb'); no Godot, test, validation script, Runner command, git mutation or sandbox bypass; the frozen worktree HEAD is S with tracked files clean and READY/packet/schema/manifest bytes unchanged; the harness left an empty directory .claude/.cc-writes in the review folder at 22:20 (no files); the 3 pattern matches are a packet read, a grep of tools/run_all_tests.py and a packet read (transcript 71851c8e-607d-4969-8a6c-13b2dc7fb54d.jsonl; tool calls {\"mcp__ccd_turn__update_plan\": 5, \"Bash\": 80, \"Read\": 4, \"Write\": 1, \"mcp__ccd_turn__wrap_up\": 1}; edit tools [\"Write\"]; pattern matches read by the governor: 3)",
"tests_or_engines_started": false,
"packet_sha256": "2ca871ef9ae7291b542f215aec2d3c0c1f524d4c8c01589d3134913dd4ee33cd",
"normal_packet_sha256": "ae8ce2ca51f6912b828ffb8a3d5e40642709d5e4c090e3d529521f2b5a11d681",
"schema_sha256": "4420e0a2f8b81ec2f0fcdddc668f5abf1a3b0d42c66171153d21eedb3b61c3e5",
"material_manifest_sha256": "2b8f91431bb16e698d52b8755311d409006da7305b713ee1a53fddbd790260a2",
"result_file": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/DESKTOP_RESULT.json",
"result_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
"result_sha256_source": "computed by the governor from the actual DESKTOP_RESULT.json (not edited)",
"status_reported": "pass",
"delivered_by": "Desktop governor session local_a7f85421-3eed-4482-8030-c0dca669573a (implementer, commander and native executor of P05-02; did not review or edit the result)",
"session_started_by": "Root (Codex) pressed the normal Start card task_e05a97cc once under existing user delegation (Root's message to the commander, 2026-10-23); the app reported the proposal started as session local_cbdd640f-21ad-4f21-8d71-12b8debccc21 (model claude-opus-5-5, effort xhigh, parent local_a7f85421); not a new human action",
"cli_authentication_recovered": false,
"cli_review_succeeded": false,
"recorded_at": 1792762619.547446
},
"independent_result": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_INDEPENDENT_REVIEW_8954914.json",
"bytes": 10653,
"sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
"copy_of": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/DESKTOP_RESULT.json"
},
"review_findings": [
{
"severity": "low",
"message": "仕入れの画面(scripts/ui/supply_management_panel.gd。allowed_paths の外)は今も「手持ち %dG」に表金だけを出し、見積の「購入後 %dG」は表金+裏金からの残りを出す。そのため表金 2・裏金 100 で 15 の仕入れでは「手持ち 2G」と「購入後 87G」が並ぶ。操作は A09 どおりで、REPAIR_03.md に差分の案と正規の扱い(範囲に含む別の登録で直す)が記録されているので、P05-02 の合否は下げない。P05-03 か仕入れ・UI の単位の登録で、この画面を allowed_paths に入れて直すこと。"
},
{
"severity": "low",
"message": "帳簿の画面の資金の欄(scripts/ui/operating_ledger_panel.gd の _cash_label)は、表金の残高を「手持ち %dG」と出し、その下に「裏金 %dG」を出す。「表金」の語は借入がある時の返済の案内にしか出ないので、借入が無い時は「手持ち」が表金だけなのか合計なのかが画面から読み取りにくい。Main から開く試験も report の cash(6210)を見るだけで、画面の文字で表金の残高を確かめていない。「表金 %dG」に揃え、その文字を試験で確かめるとよい(UI は後で入れ替える予定なので low)。"
},
{
"severity": "low",
"message": "局所の確認の生のログ(修正3の最初の失敗の p05-r3-funds-runtime-r01.log・p05-debug-funds-r02.log、修正2の 27 の pin の全て)は /private/tmp/claude-501/… の scratchpad にだけある。記録(P05_02_REPAIR2_LOCAL_CHECKS_R01.json・P05_02_REPAIR3_LOCAL_CHECKS_R01.json)は SHA を持ち今は全て一致するが、ログ自体は再起動などで消えうる。正式の証拠ではないが、元の失敗の保存のため、管理用のフォルダか artifacts の下に写しておくとよい。"
}
],
"review_high_medium": 0,
"review_low": 3,
"review_provenance_records": [
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/REVIEW_CARD_PROPOSED_8954914.json",
"bytes": 1855,
"sha256": "ee3b6d40800f0bda0afdac1ca1acf138ef3546c11c274e549a363abf0c43ce6e"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/REVIEW_STARTED_8954914_COMMANDER_RECORD.json",
"bytes": 1449,
"sha256": "6a73f3e240a548b0a44f38492ac02c14d5e9727d28472fb2e96a92e5950445c5"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/DELIVERY_INSPECT_8954914.json",
"bytes": 43791,
"sha256": "b1779f03b110e5663a8b55e05115f60a9cd09109e125093ba553e8dbabeb7d2c"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/P05_02_REVIEW_BRIEF_8954914.md",
"bytes": 14679,
"sha256": "e5817d7468eb98029974a16a5aa5c51b09c128eea704554a9c32a98be3d30c7a"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/READY_FOR_DESKTOP_REVIEW.json",
"bytes": 2693,
"sha256": "78477e7e229a195d8e0408f7eee6f022b0f99b5d7f1be5e8fabbe5d3ea881cb6"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/DESKTOP_DELIVERY.json",
"bytes": 2835,
"sha256": "472ecbc74423c3cf982750d1918701025b46317229ca20db8e9f136557a37de3"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/INTAKE_STARTED.json",
"bytes": 217,
"sha256": "d5db3ae269c619bb809bf788845e47f4fa9c3d59b34995751ae91ed21b561ec6"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/p05-02-independent-review-8954914-r01/INTAKE_FINISHED.json",
"bytes": 288,
"sha256": "a8ca9d2a819a8c58dbf31b934241f5baeb5bc7289d36d59766439ec78194b1c9"
}
],
"measurement": {
"measured_on": "895491456477b42a74e59fa239bea86c4fb9aa33",
"native": {
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/NATIVE_8954914_START.json",
"bytes": 801,
"sha256": "0d6c683c9a2794bbd357a76b9bf9f0db35df05a0b4ac33925bf0aa8fceccdf8a"
},
"native_bundle_transfer": {
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/NATIVE_8954914_BUNDLE_TRANSFER.json",
"bytes": 1335,
"sha256": "5d7dd8951c54f855487fc381360730b0f54675bf693e8c9e872c8aef5ce1592b"
},
"formal_validation_summary": {
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/FORMAL_VALIDATE_8954914_SUMMARY.json",
"bytes": 1964,
"sha256": "1fc9754fa31331aad0c47e8fca6660765f06b5e0afa7c5f72adea99cb0094311"
},
"full_roster_summary": {
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/FULL_ROSTER_8954914_R01_SUMMARY.json",
"bytes": 3151,
"sha256": "31fca47ded8d0a5c685ecc15a8a76a2a5e72e3aec14d0958485c8900f784d832"
},
"full_roster_placement": {
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/FULL_ROSTER_8954914_PLACEMENT.json",
"bytes": 958,
"sha256": "230caae320ffa4ca194111b67571c8e4e4cd77a0a6100d0aaee541cde1f8d43d"
},
"native_phases": 10,
"native_checks": 867,
"full_roster_suites": 195,
"full_roster_checks": 22539,
"measurement_not_relabelled_as_gate_Q": true,
"Q_meaning": "the evidence measured on S was checked by the registered content-based verifier in fresh checkouts of Q (integration and final); nothing was measured on Q and no engine started"
},
"capacity_records_content_preserving": [
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/P05_02_NATIVE_8954914_ALIGN_R16.json",
"bytes": 319201,
"sha256": "9a3c2b531c1fea5aeb6b6ad45d4560fbc1a34e0321289a7c72d20a406ac0ecb6"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/P05_02_VALIDATION_CHECKOUT_8954914_ALIGN_R17.json",
"bytes": 1496745,
"sha256": "1f4d4109bce6ba0d3b54239d0e09d420d8e7597a2233692e98170fb5299113e3"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/P05_02_FULL_ROSTER_8954914_PHASE061_PROFILE_COMPRESSION_R12B.json",
"bytes": 664474,
"sha256": "0ae3d93e7d130ccb3a698773f1f232a47869195622abb96a88c3c9469d94f07c"
},
{
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/P05_02_FULL_ROSTER_RAW_COMPRESSION_8954914_R18.json",
"bytes": 2368991,
"sha256": "56af5091e7545b65bfe9310658de7b6068a485843564067103e617a6dcbb32c2"
}
],
"evidence_retention_low_resolved": {
"path": "artifacts/original-completion-2026-10-02/p28-02-root-control-r01/p05-02-control-r01/P05_02_LOCAL_CHECK_RAW_LOGS_RETAINED_R01.json",
"bytes": 23020,
"sha256": "cc7c7b0a995a2378c46219f94b9d3f9a2caa7b0d693eaa71d2077b289b4cc664",
"files": 32,
"Root_rehash": "Root rehashed all 32 original and copied diagnostic logs: no differences (Root message 2026-10-23)"
},
"historical_evidence_retained": [
{
"S": "7808ca76db5f997096cdfbb0edfad0c45479e238",
"gate": "validation",
"passed": false,
"record": "Runner evidence 1"
},
{
"S": "37deb6e877e60e8f19ae4fbc64382880cf7f75bb",
"gate": "review",
"passed": false,
"status": "changes_requested",
"record": "Runner evidence 3; p05-02-independent-review-37deb6e-r02/"
},
{
"S": "d65e3da9f33d399c598ad88296e4a9d88d847a7b",
"gate": "review",
"passed": false,
"status": "changes_requested",
"record": "Runner evidence 5; p05-02-independent-review-d65e3da-r01/"
},
{
"record": "repair 2 and repair 3 local check first-run failures and prototype failures",
"manifest": "P05_02_LOCAL_CHECK_RAW_LOGS_RETAINED_R01.json"
}
],
"remaining_not_claimed": [
"P05 package stays in_progress: P05-03 (vault and bank normal operation) is planned; its three human questions are pending and no answer is inferred",
"Atomic back_gold through ordinary play is not achieved: the F1 back-facility scene registers its service_attendant by the register_staff command because normal Main cannot hire that role (P09/P11 remaining_paid_role, 2026-10-08); not closed here",
"Casino, extortion money and unit-sale revenue sources do not exist yet (disclosed in SPEC section 7)",
"Fines and events still pay from front funds only (A09 scope), disclosed without a dedicated test",
"UI lows deferred under the feature-first instruction: supply panel shows front-only on-hand next to a front+back after-purchase figure; ledger panel labels front funds as on-hand; the ledger UI is the current mock",
"AR.staff.dismissal_severance (P05, D04) is not addressed; the human severance answer is pending",
"Same Windows distribution device acceptance, long normal play and whole-game final acceptance remain"
],
"implementation_provenance": "Contract worker field codex is compatibility only; no Codex or Astra run or dispatch. Implementation, natives and full roster by the registered Claude Opus 5.5 Desktop implementer; commits made by Runner submit; independent review by a separate fresh Opus 5.5 Desktop session",
"formal_original_credit_after_ledger": 1,
"original_completed_before": 23,
"original_completed_after_ledger": 24,
"original_delivery_total": 88,
"packages_completed": 4,
"packages_total": 30,
"partial_credit": false,
"new_engine_or_full_roster_runs_for_ledger": 0,
"all_prior_failures_retained": true,
"CLI_auth_recovered": false,
"no_new_human_utterance": true
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
# P05-02 修正1: 登録の validation の出力先
- S `7808ca76db5f997096cdfbb0edfad0c45479e238` の正式の validation(`p05-funds`)は、照合の入口で失敗した(生の stderr: `AssertionError: regular final receipt required`)。native の収集(10 suites・765 checks・verify true)と build の場所への写しは成功していた。
- 原因: 登録の argv は `--output {build_dir}/p05-funds-evidence`。今の Runner は validation の argv の `{python}` だけを置き換え、`{build_dir}` は文字どおり渡し、build の場所を環境変数 `BUILD_OUTPUT_DIR` で渡す。`tools/run_p05_funds_tests.py` は P18 の runner の写しで、置き換えを Runner に任せていたため、作業ツリーの中の存在しない path を照合した。
- 修正: `tools/run_p05_funds_tests.py` は、`--output` が `{build_dir}/` で始まるとき、`BUILD_OUTPUT_DIR`(絶対 path で、symlink でない既存のディレクトリ)で置き換える。それ以外の値の扱い、収集と照合の中身、登録の argv、試験、製品のコードは変えない。
- 新しい S では native の収集をやり直す(runner の SHA が変わり、S `7808ca7` の証拠の source map と合わないため)。S `7808ca7` の収集と失敗の validation の記録は保持する。
@@ -0,0 +1,32 @@
# P05-02 修正2: 実際の経路での表裏の資金・補填・利息・税の滞納と、日次の補填の定義
- S `37deb6e877e60e8f19ae4fbc64382880cf7f75bb` の whole-unit の独立レビュー(r02)は changes_requested(指摘 1 high・2 medium・3 low)。正規の受付で `repair_pending`、commander の修正2(`COMMANDER_REPAIR_PLAN_P05_02_R02.md`、訂正の追記 `..._CORRECTION_01.md`)。S `37deb6e` までの native・全 roster・validation・review の記録と失敗は全て保持し、新しい S に流用しない。原本は 23/88 のまま。
## 試験(`tests/economy/test_p05_funds_runtime.gd` に足した場面)
- 指摘1(F1・F5)`_test_actual_service_funds`: Runtime で、表の施設(`massage_room`、通常の雇用 `staff_petra` を DAY の配置)と、登録済みの裏の札の施設(`adult_private_room`、地下。rank 2・掘削・客用の通路と階段・職員用の通路と階段を通常の command で作る)を建てて開ける。自然に来る客が自分でサービスを選び、地下の個室へは地上から歩いて降りて、完了させる(客を置く fixture は使わない)。00:00 の締めで、massage_room の `SERVICE_CASH_SWEEP` は全て `PLAYER_CASH`、adult_private_room の分は全て `BACK_CASH`(他の行き先 0)、個室の支払が自然に来た客の財布からであること、`funds.revenue_front`・`revenue_back` がその日の行き先ごとの合計と一致すること、売上が表+裏であること、世界樹ランク税が両方の売上の合計の 10%(切り捨て)で払われたこと、裏金の残高と増減がその日の裏の売上と一致することを確かめる。行き先の選択を元に戻すとこの場面は失敗する。
- 指摘2(F2):
- 人件費: 同じ場面の2日目、締めの前に表金を 5 に下げ(台帳の fixture。下げるだけ)、本物の締めが実際の雇用の給与・維持費・税を払う。`funds.back_top_up` が(給与+維持費+税−5)、給与の未払い 0、表金 0、裏金がその分減ることを確かめる。
- 自動の仕入れ `_test_operation_top_ups_and_day_total`: 倉庫に自動の仕入れ(10 個)を設定し、表金 2・裏金 1,000 で時計を進める。`advance_auto_purchases` の本物の注文が 30 で買い、領収(`SupplyAutoPurchasePlans.receipt_from_ledger`)が検証され、28 が裏金からの補填(鍵 `finance.back_top_up.purchase.<注文の鍵>`)であることを確かめる。
- 旧暦の 00:00 の維持費 `_test_legacy_midnight_top_up`: `tests/mvp/test_upkeep_legacy_midnight.gd` と同じ作り方の旧暦の save(締めが 06:00)を読み、表金 0・裏金 500 で 00:00 を越える。00:00 の維持費 1 が裏金から補填され(鍵 `finance.back_top_up.midnight.1440`)、滞納 0、06:00 の締めの report がその補填を数えることを確かめる。
- 指摘3(F4): 同じ `_test_operation_top_ups_and_day_total` で、1,000 を借り(台帳の fixture。借入の発生源は無い)、返済の command(`repay_loan_principal`)で 200 を返す(表金 0 なので裏金で払う)。その日の締めの利息が返済の後の残高 800 の 1% の 8 で、払われることを確かめる。
- 指摘4(F3)`_test_tax_arrears_next_day`: 売上 200 の日に表裏の資金が無く、税 20 が未払いのまま繰り越す(日次の滞納と Operations の report に出る)。翌日の締めで古い 20 と新しい 10 を払い、税の未払いが 0 になることを確かめる。
- 各場面の最後に save の写しから戻し、そのまま一致することを確かめる。
## 日次の補填の定義(指摘5)と、保存済みの report の互換
- `funds.back_top_up` は、その営業日に記帳された全ての補填(操作の時の仕入れ・自動の仕入れ・返済、旧暦の 00:00 の維持費、締めの補填)の合計にした(`autoload/simulation_runtime.gd` の `_daily_fund_report`)。営業日の区切りは売上の集計と同じ(period_start 以上・締めの分より前)。締めの補填は鍵で数え、前日の締めの補填(period_start の分に記帳)は数えない。帳簿の画面の締めの欄「裏金から表金へ補填」はこの値を出す。
- report に定義の版 `fund_report_version: 2` を入れた。版の無い report は、S `7808ca7`・`37deb6e`(統合されていない)で保存された、締めの補填だけを数えた定義(版1)。読込の照合(`settlement.funds` と台帳から作り直した report の比較)は、保存された report が書かれた定義で比べる。版の無い report は版1の式でだけ受け、新しい式の値を持つ版の無い report、版2の印で締めの分だけの値の report、知らない版の report は拒む(緩めない)。A12(資金を分ける前の save)は report を持たないので、今までどおり(帳簿の記帳が無いことを確かめる)。版1の report は書かれた時のまま残し、画面は「(締めの時の分のみ)」と出す。
- 試験: 同じ日に注文・返済・締めの3つの補填がある save から、版1の形の report(印を外し、締めの分の値)を作って読めること、その値と表示が残ること、上の3つの不正な形が拒まれ何も変えないことを確かめる。
## 開示(指摘6 と、修正2で分かったこと)
- 今のゲームで裏金に入る売上の発生源は、登録済みの施設では `adult_private_room` だけ。`black_market`(trade。原子要件 income_trade のユニット売却に当たる)と `vip_private_salon` は拡張の定義で既定では登録されない。カジノ・脅迫金・ユニット売却の発生源は無い。原子要件 income_casino・income_blackmail・income_trade は未達のままで、この開示で完了にはしない。
- 定義が引けない施設の売上は、既存の挙動どおり表金に入る(裏の札の判定ができないため。`_apply_service_transaction` の注記)。定義の無い施設を扱う既存の経路は変えない。
- F1 の実際の売上の経路の試験で、`adult_private_room` の担当(役割 `service_attendant`)は Runtime の `register_staff` の command で登録した職員(fixture の職員。給与 0 の FREE_HIRE)。通常の雇用の候補にこの役割は無く、`_staff_role_matches_facility` にも代わりの役割は無いので、通常の Main(雇用と配置の画面)ではこの施設に職員を置けず、裏の売上を得られない。これは原 work の P09・P11 の `current_paid_role_compatibility_checkpoint.remaining_paid_role`(「adult_private_room/service_attendant has no canonical paid hire candidate」、2026-10-08、`work_packages.json`)に残っている原残件で、通常の Main での全施設の営業は未達。P05-02 では職員の候補・役割・給与・定義を作らず、P09・P11 の残件はこの開示で閉じない。
- `adult_private_room` の定義の `unlock_rank` 3・`research_requirements`(discreet_hospitality)・`opening_schedule`(夜だけ)は、今の Runtime では建設や営業の条件として使われていない(定義の検査だけ)。試験は rank 2(地下の開放)で建て、夜に営業させた。P05-02 では変えない。
- 補填の元にした表金の減少(台帳の fixture)と借入(台帳の fixture)は、残高を作るだけで、確かめる支払・補填・利息・税は本物の経路。
## 変えていないもの
- 登録の argv、`tools/run_p05_funds_tests.py`、狭い roster(8 suites+再開の2段、実 10 段)、試験の既存の期待値(`EXPECTATION_CHANGES.json` に足すものは無い)。
@@ -0,0 +1,24 @@
# P05-02 修正3: 通常の帳簿の画面の返済(A09)と、残りの試験と開示
- S `d65e3da9f33d399c598ad88296e4a9d88d847a7b` の whole-unit の独立レビュー(fresh、local_92520fb7)は changes_requested(medium 1・low 4)。F1〜F8 は証拠の上で満たすとされ、残りは下の指摘。正規の受付で `repair_pending`、commander の修正3(`COMMANDER_REPAIR_PLAN_P05_02_R03.md`、event 28、repairs 3)。S `d65e3da` までの native・全 roster・validation・review・容量の記録と失敗は全て保持し、新しい S に流用しない。原本は 23/88 のまま。
## 直したもの
- 指摘1(medium、F2 と横断条件「原本条件と UI 操作の一致」): 通常の帳簿の画面(`scripts/ui/operating_ledger_panel.gd`)の返済の上限・案内・拒否を、表金だけ(`cash`)から表金+裏金(`FinancePolicy.available_funds`)にした。`SimulationRuntime.get_loan_principal_report` に `back_cash` と `available_funds` を足した(`cash` は今までどおり表金)。案内は「表金 xG・裏金 yG(計 zG)から元本を返済します。表金で足りない分は裏金から補填します。」と出し、完済に足りない時は「完済には表金と裏金の合計が不足しています。」と出す。返済の command は今までどおり Runtime の `repay_loan_principal` で、表金から先に払い、残りを裏金からの補填と同じ1つの取引で記帳する(A09)。既存の試験が確かめる文言(「借入はありません」「受理済み」「実行できません」「…Gを返済」)は残した。
- 試験 `_test_ledger_repayment_uses_hidden_funds`: 通常の帳簿の画面の部品で、表金 20・裏金 300 の時に 200 の返済が押せ、1つの取引(裏金からの補填 180 → 元本 200)になること、残元本 800 の完済は表金+裏金 120 では押せず、500 の指定も押せず、押しても状態が変わらないこと、裏金が 820 になると完済が押せ、1つの取引(補填 800 → 元本 800)で完済し、表金 0・裏金 20・Gold が表金を写すこと、save から戻してそのまま一致すること。
- 指摘2(low): 税と利息の滞納も §3.8 の `arrears_total` に入るので、建設と投資の制限にも入る(開示)。締めの日次の記録の `arrears_total` に税・利息の滞納が入り、Operations の調整役の判定(`can_start_construction_or_investment`)が `ARREARS_REMAIN` で止める。その後の Runtime の判定(`_operations_construction_investment_gate`。`_operations_total_arrears` に `RANK_TAX_PAYABLE` と `LOAN_INTEREST_PAYABLE` の残高が入る)も、残高が残る間は `UNPAID_EXPENSES` で止める。試験 `_test_tax_arrears_next_day`: 税の滞納 20 がある日に建設の command が `ARREARS_REMAIN`(arrears 20)で拒まれ、翌日の締めで払った後に建てられる(表金から 20)こと(局所の確認で実際の理由が `ARREARS_REMAIN` だったので、レビューの指摘の `UNPAID_EXPENSES` ではなくそれを確かめる)。
- 指摘3(low): 試験を足した。表金 0・裏金 663 の時に建設(`guest_corridor`、20)が拒まれ、裏金が減らず、補填が増えないこと(建設は A09 の対象外。`_test_operation_top_ups_and_day_total`)。税の滞納が残る日に、帳簿の画面の締めの欄が「未払い 税 20G・利息 0G」、今日の欄が「(未払い 20G)」と出すこと(`_test_tax_arrears_next_day`)。罰金・event の支払など他の対象外の支払は、コードで表金だけから払うことを確かめたが試験は無い(開示)。
## 文言の補足(指摘4、low)
- 凍結の SPEC(契約の `spec_sha256`)と DB の契約は変えない。SPEC §3.5 の「元本の出入り(financing_principal_*)は今までどおり売上と経費に入れない」は、原本どおり次の意味で読む: 借入の元本の入金は売上に入れない(F4)。元本の返済は原本どおり日次の経費(`financing_principal_outflows`。`scripts/operations/operations_coordinator.gd` の `_net_profit` が日次の利益から引き、帳簿の画面は「元本返済(決算経費)」と出す。原子要件の借金返済の経費)。この単位はこの挙動を変えていない。
## 範囲の外で直していないもの(指摘5、low)
- 仕入れの画面 `scripts/ui/supply_management_panel.gd` は契約の allowed_paths の外なので、この単位では直さない。今の画面は「手持ち %dG」に表金だけを出し、見積の「購入後 %dG」は preview の `cash_after`(A09 で表金+裏金からの残り)を出すので、例えば表金 2・裏金 100 で 15 の仕入れでは「手持ち 2G」と「購入後 87G」が並ぶ。操作は A09 どおりに通る。
- 具体的な差分の案: 「手持ち %dG」を「表金 %dG・裏金 %dG」に分けて出し(`economy_service.get_balance(FinancePolicy.BACK_CASH)`)、「購入後 %dG」を「購入後(表金+裏金)%dG」とし、表金で足りない分を裏金から補うことを一言出す。
- 正規の扱い: この画面を範囲に含む別の登録(仕入れ・UI の単位、または P05-03 の登録で allowed_paths に入れる)で直す。それまでは未修正で、P05-02 の完了に数えない。
## 変えていないもの
- 登録の argv、`tools/run_p05_funds_tests.py`、狭い roster(8 suites+再開の2段、実 10 段)、試験の既存の期待値(`EXPECTATION_CHANGES.json` に足すものは無い)、凍結の SPEC、DB の契約。
@@ -243,7 +243,7 @@
"facility_value_contracts_validation.json": "bf490a9ca585c48fc318e76c4a069ef244827d828d46e6415399f7710590dd16", "facility_value_contracts_validation.json": "bf490a9ca585c48fc318e76c4a069ef244827d828d46e6415399f7710590dd16",
"performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762", "performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762",
"progress_contract.json": "ccbdfce8c8365d5c13f59531955708834cce45acd0e136561c95295dfdb5df77", "progress_contract.json": "ccbdfce8c8365d5c13f59531955708834cce45acd0e136561c95295dfdb5df77",
"progress_snapshot.json": "b90224aa67f82882fb5c5d52b1fd22065599bf3ab155f28e97670d84a64ca563", "progress_snapshot.json": "38777ebda6729c8127fcca0eb6ecf133673445b8d66ebb51b44cad00e5457b1a",
"progress_validator_guard_checks.json": "72ee513fcf1cad85c24bfd0a4b472f425f6acf117928ece0f6b41e29018da627", "progress_validator_guard_checks.json": "72ee513fcf1cad85c24bfd0a4b472f425f6acf117928ece0f6b41e29018da627",
"progress_validator_guard_checks.r01.json": "14c26adffa5a1938241db4912e50ea54b660641ee6fee2e412d467dce14d0519", "progress_validator_guard_checks.r01.json": "14c26adffa5a1938241db4912e50ea54b660641ee6fee2e412d467dce14d0519",
"requirement_coverage.json": "03b10e4da367579db502d0671a6e0dda36fd385111c6f1c0084acaff7605f593", "requirement_coverage.json": "03b10e4da367579db502d0671a6e0dda36fd385111c6f1c0084acaff7605f593",
@@ -253,6 +253,6 @@
"validate_facility_value_contracts.py": "91640e2a76cd6cc9186f7b85c0bf379b24b6cac0d48962815235ee628e7ae43e", "validate_facility_value_contracts.py": "91640e2a76cd6cc9186f7b85c0bf379b24b6cac0d48962815235ee628e7ae43e",
"validate_plan.py": "dc65987db520df75a9b90037eae82819ba4c8863354340681092703454cdfb31", "validate_plan.py": "dc65987db520df75a9b90037eae82819ba4c8863354340681092703454cdfb31",
"validator_update_checks.json": "f997d0f3de2cea29550840b1d5cb23647d64f81329de46758551e2b6c8fecc27", "validator_update_checks.json": "f997d0f3de2cea29550840b1d5cb23647d64f81329de46758551e2b6c8fecc27",
"work_packages.json": "445870757a77382bb8591ac4f333838a4db45203e02f131b497ca86d2cbe3dfa" "work_packages.json": "f543f39842764076b4522cc818c476d18bdc8e09ac9d83e02dd84ccadc966496"
} }
} }
@@ -1,9 +1,9 @@
{ {
"schema_version": 1, "schema_version": 1,
"integrated_at_jst": "2026-10-22T10:14:42.989122+09:00", "integrated_at_jst": "2026-10-23T22:51:54.402572+09:00",
"scope": "原30パッケージ/88成果、正式完了23成果・4パッケージ。部分点なし。P28-01全条件を独立レビュー/通常final/実DB結合で正式完了。次P28-02未完。全ゲーム/同一Windows最終受入は未完。", "scope": "原30パッケージ/88成果、正式完了24成果・4パッケージ。部分点なし。P05-02全条件を独立レビュー/通常final/実DB結合で正式完了(P05 packageはP05-03未完)。ユーザー指示により機能実装の完了を優先し、P28-02の最適化と試験速度の調整は後段。全ゲーム/同一Windows最終受入は未完。",
"source_head": "4e34e5209706e3e16a4860e65e52b90287722c36", "source_head": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"source_authority_note": "P28-02-EQの実Sを測定しQのsource/settings/EQ/DIAG/collector bytes一致資格を確認。全4通常ゲートと実DB結合完了、EQ補助の原加算0。旧Sの失敗とchanges_requestedは元証拠に保持。", "source_authority_note": "台帳更新直前main Qcb183fd(S8954914とtree同一)。P05-02はRunner completed 8/8・repairs3、validation/review/integration/final全PASS、final ec32bfcd SHA f8d25e28 verified true/engines_started0。別fresh Desktop Opus5.5がF1〜F8 PASS(high/medium0・low3)。Rootが既存の人のmain/push許可を正確なQ/Mへ正直に適用(新しい人の発言は作らない)。測定はSでQへ再ラベルしない。原24/88・4/30。",
"immutable_history": { "immutable_history": {
"baseline.json": { "baseline.json": {
"sha256": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b", "sha256": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b",
@@ -160,8 +160,8 @@
"in_progress": 26 "in_progress": 26
}, },
"current_delivery_state_counts": { "current_delivery_state_counts": {
"completed": 23, "completed": 24,
"in_progress": 18, "in_progress": 17,
"planned": 47 "planned": 47
}, },
"completed_current_deliveries": [ "completed_current_deliveries": [
@@ -172,6 +172,7 @@
"P04-02", "P04-02",
"P04-03", "P04-03",
"P05-01", "P05-01",
"P05-02",
"P06-01", "P06-01",
"P06-02", "P06-02",
"P06-03", "P06-03",
@@ -5306,7 +5307,7 @@
"Fresh independent Opus all6, normal candidate qualification/approval/integration/final", "Fresh independent Opus all6, normal candidate qualification/approval/integration/final",
"Original P28-02 T1 actual optimization, same-condition3 repeated comparisons, RESTART, exact final full roster, whole-unit original work/AC plus cross-cutting6 review/final" "Original P28-02 T1 actual optimization, same-condition3 repeated comparisons, RESTART, exact final full roster, whole-unit original work/AC plus cross-cutting6 review/final"
], ],
"original_completed": 23, "original_completed": 24,
"original_total": 88, "original_total": 88,
"new_optimization_accepted": false, "new_optimization_accepted": false,
"current_support_stage": { "current_support_stage": {
@@ -5323,7 +5324,8 @@
"sha256": "de4adb29bb8a2ec6f2426bdb4c23d5094bdfea764eb045ef33af4948625a45d8" "sha256": "de4adb29bb8a2ec6f2426bdb4c23d5094bdfea764eb045ef33af4948625a45d8"
}, },
"original_credit": 0 "original_credit": 0
} },
"deferred_by_user_feature_first_instruction": true
}, },
"current_P14_01_contract_audit": { "current_P14_01_contract_audit": {
"audit": "artifacts/original-completion-2026-10-02/p14-01-current-contract-audit-r01/audit.json", "audit": "artifacts/original-completion-2026-10-02/p14-01-current-contract-audit-r01/audit.json",
@@ -9794,13 +9796,13 @@
"p24_03_formal_closure": "docs/plans/original-completion-2026-10-02/execution/p24-03-formal-closure-r01/closure.json", "p24_03_formal_closure": "docs/plans/original-completion-2026-10-02/execution/p24-03-formal-closure-r01/closure.json",
"new_engine_run_scope": "Supplemental positive metadata diagnostic r01 failed cleanup preparation/log isolation; r02 passed17checks unchanged source and entire default profile. No duplicated full game regression.", "new_engine_run_scope": "Supplemental positive metadata diagnostic r01 failed cleanup preparation/log isolation; r02 passed17checks unchanged source and entire default profile. No duplicated full game regression.",
"current_closure_targets": { "current_closure_targets": {
"formally_closed": "P28-01", "formally_closed": "P05-02",
"next_candidate": "P28-02", "next_candidate": "P28-02",
"formal_count": 23, "formal_count": 24,
"original_delivery_units": 88, "original_delivery_units": 88,
"partial_credit": false, "partial_credit": false,
"selection_status": "P28-01/DIAG formal; retained S4 finishing actual static-result defect and final local submission. Original P28-02 unfinished.", "selection_status": "P05-02正式完了。ユーザー指示により機能実装を優先し、回答待ちに依らない次の機能実装をRootの通常登録へ提案する。P28-02の記録は保持し後段。",
"pending_decisions": "No human decision pending. Follow same actual r03 run; after submitted S collect exact native7, both validations, fresh independent Opus all6 and normal final." "pending_decisions": "人への問いの回答待ち: P05-03の3件、P07-01の天候、P16-01-DISMISSの退職金。どれも推定しない。"
}, },
"historical_regression_before_P08_02_formal_closure": { "historical_regression_before_P08_02_formal_closure": {
"path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-final-candidate-r32-combined-root-acceptance-r01.json", "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-final-candidate-r32-combined-root-acceptance-r01.json",
@@ -10598,7 +10600,7 @@
}, },
"whole_game_acceptance": false "whole_game_acceptance": false
}, },
"next_closure_note": "原P28-02は意味を保つ軽量化の全条件で閉じる。補助EQは正式完了(原加算0)。続くT1実最適化/同条件3反復/RESTART/正確最終全192/原work・ACと横断6のwhole-unit受入後だけ23→24。", "next_closure_note": "P05-02正式完了(24/88)。機能実装を優先:回答待ちに依らない次の機能の補助作業をRootが通常登録。P16-01-DISMISS r03は退職金の回答待ち。P28-02-OPTは後段(記録は保持)。",
"current_p26_01_content_ledger": { "current_p26_01_content_ledger": {
"at": "2026-10-17T20:44:25.180745+09:00", "at": "2026-10-17T20:44:25.180745+09:00",
"original_plan_id": "P26-01", "original_plan_id": "P26-01",
@@ -10788,16 +10790,24 @@
"current_workflow_handoff": { "current_workflow_handoff": {
"controller_local_session": "local_a7f85421-3eed-4482-8030-c0dca669573a", "controller_local_session": "local_a7f85421-3eed-4482-8030-c0dca669573a",
"actual_model": "Opus5.5", "actual_model": "Opus5.5",
"completed": "P28-02-EQ", "project": "the-tree-p05-02",
"next": "P28-02-OPT per OPT_CONTRACT_T1 §6: before record (S_eq native qualified on Q_eq), (b) before runs, OPT registration and implementation, after measurements on the OPT S, OPT validation, whole-unit review, integration/final, ledger 23->24", "task": "P05-02",
"checkpoint": { "state": "completed",
"path": "docs/plans/original-completion-2026-10-02/execution/p28-02/EQ_FORMAL_COMPLETION_4E34E52.json", "submission": "895491456477b42a74e59fa239bea86c4fb9aa33",
"bytes": 23096, "candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"sha256": "833f29fd97f54a2150a3e674ba45353f938207f57b59687cfa97f1c65caeea7a" "main_base": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"evidence": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_FORMAL_COMPLETION_CB183FD.json",
"bytes": 30318,
"sha256": "27e197df6c636126ad74f0d3a30041134c4bd4a1c94415f9ab4c380195bf100e"
}, },
"original_credit": 0, "independent_reviewer_session": "local_cbdd640f-21ad-4f21-8d71-12b8debccc21",
"original_completed": 23, "independent_result_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
"original_total": 88 "all4_normal_gates_passed": true,
"original_credit": 1,
"original_completed": 24,
"original_total": 88,
"next": "Feature-first: next feature implementation that needs no pending human answer, proposed to Root for normal registration; P16-01-DISMISS r03 waits for the severance answer; P28-02-OPT deferred by the user instruction"
}, },
"historical_workflow_before_desktop_handoff_20261019": { "historical_workflow_before_desktop_handoff_20261019": {
"source_thread": "01a14f6a-b6a1-7fd3-bd45-0f84bbef77be", "source_thread": "01a14f6a-b6a1-7fd3-bd45-0f84bbef77be",
@@ -14205,5 +14215,101 @@
"path": "docs/plans/original-completion-2026-10-02/execution/p28-02/EQ_FORMAL_COMPLETION_4E34E52.json", "path": "docs/plans/original-completion-2026-10-02/execution/p28-02/EQ_FORMAL_COMPLETION_4E34E52.json",
"bytes": 23096, "bytes": 23096,
"sha256": "833f29fd97f54a2150a3e674ba45353f938207f57b59687cfa97f1c65caeea7a" "sha256": "833f29fd97f54a2150a3e674ba45353f938207f57b59687cfa97f1c65caeea7a"
},
"historical_before_P05_02_formal_cb183fd": {
"integrated_at_jst": "2026-10-22T10:14:42.989122+09:00",
"scope": "原30パッケージ/88成果、正式完了23成果・4パッケージ。部分点なし。P28-01全条件を独立レビュー/通常final/実DB結合で正式完了。次P28-02未完。全ゲーム/同一Windows最終受入は未完。",
"source_head": "4e34e5209706e3e16a4860e65e52b90287722c36",
"source_authority_note": "P28-02-EQの実Sを測定しQのsource/settings/EQ/DIAG/collector bytes一致資格を確認。全4通常ゲートと実DB結合完了、EQ補助の原加算0。旧Sの失敗とchanges_requestedは元証拠に保持。",
"current_delivery_state_counts": {
"completed": 23,
"in_progress": 18,
"planned": 47
},
"completed_current_deliveries": [
"P00-01",
"P02-01",
"P02-02",
"P04-01",
"P04-02",
"P04-03",
"P05-01",
"P05-02",
"P06-01",
"P06-02",
"P06-03",
"P08-02",
"P08-03",
"P13-03",
"P14-01",
"P14-02",
"P14-03",
"P19-03",
"P22-01",
"P23-01",
"P23-03",
"P24-03",
"P26-01",
"P28-01"
],
"current_closure_targets": {
"formally_closed": "P28-01",
"next_candidate": "P28-02",
"formal_count": 23,
"original_delivery_units": 88,
"partial_credit": false,
"selection_status": "P28-01/DIAG formal; retained S4 finishing actual static-result defect and final local submission. Original P28-02 unfinished.",
"pending_decisions": "No human decision pending. Follow same actual r03 run; after submitted S collect exact native7, both validations, fresh independent Opus all6 and normal final."
},
"current_workflow_handoff": {
"controller_local_session": "local_a7f85421-3eed-4482-8030-c0dca669573a",
"actual_model": "Opus5.5",
"completed": "P28-02-EQ",
"next": "P28-02-OPT per OPT_CONTRACT_T1 §6: before record (S_eq native qualified on Q_eq), (b) before runs, OPT registration and implementation, after measurements on the OPT S, OPT validation, whole-unit review, integration/final, ledger 23->24",
"checkpoint": {
"path": "docs/plans/original-completion-2026-10-02/execution/p28-02/EQ_FORMAL_COMPLETION_4E34E52.json",
"bytes": 23096,
"sha256": "833f29fd97f54a2150a3e674ba45353f938207f57b59687cfa97f1c65caeea7a"
},
"original_credit": 0,
"original_completed": 23,
"original_total": 88
},
"next_closure_note": "原P28-02は意味を保つ軽量化の全条件で閉じる。補助EQは正式完了(原加算0)。続くT1実最適化/同条件3反復/RESTART/正確最終全192/原work・ACと横断6のwhole-unit受入後だけ23→24。",
"next_single_delivery_unit.original_completed": 23
},
"current_P05_02_formal_completion": {
"state": "completed",
"S": "895491456477b42a74e59fa239bea86c4fb9aa33",
"Q": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"M": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"all4_normal_gates_passed": true,
"proof": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_FORMAL_COMPLETION_CB183FD.json",
"bytes": 30318,
"sha256": "27e197df6c636126ad74f0d3a30041134c4bd4a1c94415f9ab4c380195bf100e"
},
"independent_review": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_INDEPENDENT_REVIEW_8954914.json",
"bytes": 10653,
"sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5"
},
"acceptance_record": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_ACCEPTANCE_CB183FD.md",
"bytes": 5848,
"sha256": "98d9e202dcb575bb6882ff7abf73f152307ac606aa1a73b545c67f80d4f7cdde"
},
"original_credit": 1,
"formal_original_completed": 24,
"original_total": 88,
"remaining_not_claimed": [
"P05 package stays in_progress: P05-03 planned; its three human questions are pending and no answer is inferred",
"Atomic back_gold through ordinary play not achieved: the F1 back-facility attendant is registered by register_staff because normal Main cannot hire service_attendant (P09/P11 remaining_paid_role, 2026-10-08)",
"Casino, extortion and unit-sale revenue sources absent (SPEC section 7 disclosure); fines and events pay from front funds only, disclosed without a dedicated test",
"Review low: supply panel mixes front-only on-hand with a front+back after-purchase figure (outside allowed_paths); deferred under the feature-first instruction",
"Review low: ledger panel labels front funds as on-hand; the ledger UI is the current mock to be replaced; deferred under the feature-first instruction",
"AR.staff.dismissal_severance (P05, D04) not addressed; human severance answer pending",
"Same Windows distribution device acceptance, long normal play and whole-game final acceptance remain"
]
} }
} }
@@ -5230,7 +5230,7 @@
}, },
"legacy_state": "in_progress", "legacy_state": "in_progress",
"state_scope": "package-level; completed means whole package accepted; partial implementation is not completion", "state_scope": "package-level; completed means whole package accepted; partial implementation is not completion",
"current_summary": "P05-01は正式受入済み、P05-02/03は未完。P04は通常候補b1810ff4で閉鎖し、原30/88の正式完了は17。次候補P05-02では全費目・表裏資金・3営業日照合・元本売上外を維持し、D04/資金配分・保存移行・支払規約の未確定条件と既存証拠の現版適用を実Opusが再監査して選定する。旧1299時点の候補・通常OS証拠は履歴として保全し、未検証の現版へ自動転用しない。新規実装は未起動。", "current_summary": "P05-01・P05-02は正式受入済み(P05-02はS8954914→Qcb183fd、独立Opus PASS、通常4ゲートPASS)。P05-03「金庫と銀行の通常営業」は未完(人への問い3件は回答待ち)で、package全体はin_progress。通常操作の裏金(back_gold)・退職金・UI low2・Windows/全ゲーム受入は未達を維持。",
"remaining_deliverables": [ "remaining_deliverables": [
{ {
"id": "P05-01", "id": "P05-01",
@@ -5569,7 +5569,7 @@
"title": "全費目と資金区分", "title": "全費目と資金区分",
"work": "表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税を帳簿へ接続する。", "work": "表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税を帳簿へ接続する。",
"acceptance": "3営業日で資金増減と台帳が一致し、借入元本を売上に含めない。", "acceptance": "3営業日で資金増減と台帳が一致し、借入元本を売上に含めない。",
"state": "in_progress", "state": "completed",
"evidence": [ "evidence": [
{ {
"path": "artifacts/original-completion-2026-10-02/p05-02-current-condition-audit-r02/condition-matrix.json", "path": "artifacts/original-completion-2026-10-02/p05-02-current-condition-audit-r02/condition-matrix.json",
@@ -5642,17 +5642,89 @@
"sha256_at_integration": "b68f4ca4af0feb5fc3c2ae040682e2f2a0a2f1651bd8384d0a0a1e4c96aee5d8", "sha256_at_integration": "b68f4ca4af0feb5fc3c2ae040682e2f2a0a2f1651bd8384d0a0a1e4c96aee5d8",
"scope": "Finite disclosed loan fixture, two actual closes and normal OS long-ledger display; no product adoption or P05-02 completion", "scope": "Finite disclosed loan fixture, two actual closes and normal OS long-ledger display; no product adoption or P05-02 completion",
"linked_at_jst": "2026-10-10T14:53:44.978390+09:00" "linked_at_jst": "2026-10-10T14:53:44.978390+09:00"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_FORMAL_COMPLETION_CB183FD.json",
"bytes": 30318,
"sha256": "27e197df6c636126ad74f0d3a30041134c4bd4a1c94415f9ab4c380195bf100e",
"scope": "Original P05-02 whole-unit formally completed; all4 normal gates, actual separate fresh Opus F1-F8 PASS, original+1 after ledger",
"linked_at_jst": "2026-10-23T22:51:54.402572+09:00"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_INDEPENDENT_REVIEW_8954914.json",
"bytes": 10653,
"sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
"scope": "Actual fresh independent Opus result for S8954914 (byte copy), pass, high/medium 0, low 3",
"linked_at_jst": "2026-10-23T22:51:54.402572+09:00"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_ACCEPTANCE_CB183FD.md",
"bytes": 5848,
"sha256": "98d9e202dcb575bb6882ff7abf73f152307ac606aa1a73b545c67f80d4f7cdde",
"scope": "Readable acceptance record of the same completion with retained failures and open residuals",
"linked_at_jst": "2026-10-23T22:51:54.402572+09:00"
} }
], ],
"progress_summary": "次の優先候補。P04はb1810ff4で正式受入済み。全費目・表裏資金・3営業日照合・元本売上外という原条件は維持。既存1299時点の有限証拠と表示候補は履歴として保持し、現版への適用は未監査。D04・資金区分/移行/支払規約の未確定値を捏造せず、実Opusによる次項目選定と既存Runner/成果監査を待つ。新規実装は未起動。", "progress_summary": "P05-02原work/acceptance/横断6を受入8件(F1〜F8)の別fresh Desktop Opus5.5独立レビューPASS(高/中0・low3)、通常validation/review/integration/final全PASSと実DB/traceability結合で正式完了。S8954914→Qcb183fd(tree同一)。測定はS(native10段867・全roster195/22539)、Qは中身同一の照合で測定と書かない。正式24/88・4/30、部分点なし。P05 package(P05-03)・通常操作の裏金(back_gold、P09/P11のremaining_paid_role)・UI low2・退職金・Windows/全ゲーム受入は未達を維持。",
"remaining_gaps": [ "remaining_gaps": [],
"表金/裏金の初期5000G・旧Gold割当、費目別支払元・不足時規約を確定し、記帳/保存移行/通常UIを接続する。", "updated_at_jst": "2026-10-23T22:51:54.402572+09:00",
"D04の税ランク別額または算式・精算時点、利息算式/対象/精算時点等の必要条件を確定し、実producer/分類/支払契約/保存再開を実装する。", "historical_progress_summary_before_P04_closure": "P05-02を現1299/source df8328eaで着手。原work/acceptanceと88成果単位を維持し、現全回帰の財務10試験655/0の本文・raw・実PID・marker・終了・private profileをRootと同1名が読取照合して再使用。現版は単一PLAYER_CASHで表金/裏金未接続。税/利息はaggregate0のみでproducer/category mapping未実装、D04本番条件と資金割当/旧Gold移行/支払規約の回答待ち。旧v5正式再開証拠は現版との差12refsにより自動転用しない。既存維持費/仕入れ/給与/元本契約は再使用可能だが全費目3営業日統合受入は未完。各費目を毎日非ゼロ/部分払/未払にする追加条件は設けない。既存確定費目の個別表示1ref候補を準備、本体未採用。製品変更0・重複全回帰0・正式完了追加0。Windows実機で本単位全体をブロックしない。 表示候補UI1refは本体未採用のまま、現1299に候補SHA19145a61を限定適用した隔離stageでparse・公開関連2suite142/0を確認。表示専用fixture31/0は異なる4費目、旧summary欠損key、日選択/refresh/最新への復帰を確認し4表示操作の全Runtime varbytes前後一致。値は本番税利息政策/実producer/3営業日証拠へ換算しない。通常OS Main PID24687で受付500G/資材8建設、通常completion、00:00維持費発生2G/支払2G/未払0/利益-2/手持4498を実観測。1280x720の借入なし1施設1決算で表/裏の4費目・施設別表示が可視。1461 paused front→back→frontは表示層active_layerの一byte0→1以外全literal一致、表への復帰全一致、通常close前後全一致。externalOStraceはRootの実CUA呼出忠実転記、独立hardwarecaptureではない。先行QA不備の負記録保持。同1名が有限証拠を独立受入、借入あり長表示/スクロールの有限確認は後段を参照、統合最終版全回帰は未実行。製品変更0/重複全回帰0/正式完了追加0、未確定資金規約・D04/全費目3日受入を保持。 同一表示候補SHA19145a61の隔離stageで、既存公開APIを用いた明示fixtureの借入元本1000G/返済400Gと有償倉庫2施設を準備し、実際の2締めを経て正式4member保存。native PID28624 14/0、通常OS Main PID29022でmanual_ui読込、日付選択、元本の折返し、4費目、施設別2行、実帳簿スクロール、返済100G未送信入力、上端復帰を確認。現金4692G/残元本600G、第1日維持費2G/利益-402G、第2日維持費6G/利益-6G/前日比396G。読込後7checkpointとpreclose/close全9varが600568bytesでliteral一致。実PNGとpure observer/strict native/正式保存handoff不変を同1名が独立レビュー。fixtureの元本額は本番融資条件ではなく、自然借入・税利息政策/producer・表裏資金・全費目3日受入へ換算しない。製品変更0/候補未採用/追加全回帰0/正式完了追加0。",
"全費目/全資金区分の3営業日全資金増減・台帳照合、元本売上外、適用する不足/失敗/再送境界を代表シナリオで検証する。", "historical_before_formal_cb183fd": {
"借入あり2締め2施設の通常Viewport/折返し/スクロール/元本操作部は有限確認済み。関連金融変更を固めた正確な最終候補で必須全回帰と独立採用/全条件受入を行い正式台帳へ反映する。" "state": "in_progress",
], "progress_summary": "次の優先候補。P04はb1810ff4で正式受入済み。全費目・表裏資金・3営業日照合・元本売上外という原条件は維持。既存1299時点の有限証拠と表示候補は履歴として保持し、現版への適用は未監査。D04・資金区分/移行/支払規約の未確定値を捏造せず、実Opusによる次項目選定と既存Runner/成果監査を待つ。新規実装は未起動。",
"updated_at_jst": "2026-10-19T00:54:23.322453+09:00", "remaining_gaps": [
"historical_progress_summary_before_P04_closure": "P05-02を現1299/source df8328eaで着手。原work/acceptanceと88成果単位を維持し、現全回帰の財務10試験655/0の本文・raw・実PID・marker・終了・private profileをRootと同1名が読取照合して再使用。現版は単一PLAYER_CASHで表金/裏金未接続。税/利息はaggregate0のみでproducer/category mapping未実装、D04本番条件と資金割当/旧Gold移行/支払規約の回答待ち。旧v5正式再開証拠は現版との差12refsにより自動転用しない。既存維持費/仕入れ/給与/元本契約は再使用可能だが全費目3営業日統合受入は未完。各費目を毎日非ゼロ/部分払/未払にする追加条件は設けない。既存確定費目の個別表示1ref候補を準備、本体未採用。製品変更0・重複全回帰0・正式完了追加0。Windows実機で本単位全体をブロックしない。 表示候補UI1refは本体未採用のまま、現1299に候補SHA19145a61を限定適用した隔離stageでparse・公開関連2suite142/0を確認。表示専用fixture31/0は異なる4費目、旧summary欠損key、日選択/refresh/最新への復帰を確認し4表示操作の全Runtime varbytes前後一致。値は本番税利息政策/実producer/3営業日証拠へ換算しない。通常OS Main PID24687で受付500G/資材8建設、通常completion、00:00維持費発生2G/支払2G/未払0/利益-2/手持4498を実観測。1280x720の借入なし1施設1決算で表/裏の4費目・施設別表示が可視。1461 paused front→back→frontは表示層active_layerの一byte0→1以外全literal一致、表への復帰全一致、通常close前後全一致。externalOStraceはRootの実CUA呼出忠実転記、独立hardwarecaptureではない。先行QA不備の負記録保持。同1名が有限証拠を独立受入、借入あり長表示/スクロールの有限確認は後段を参照、統合最終版全回帰は未実行。製品変更0/重複全回帰0/正式完了追加0、未確定資金規約・D04/全費目3日受入を保持。 同一表示候補SHA19145a61の隔離stageで、既存公開APIを用いた明示fixtureの借入元本1000G/返済400Gと有償倉庫2施設を準備し、実際の2締めを経て正式4member保存。native PID28624 14/0、通常OS Main PID29022でmanual_ui読込、日付選択、元本の折返し、4費目、施設別2行、実帳簿スクロール、返済100G未送信入力、上端復帰を確認。現金4692G/残元本600G、第1日維持費2G/利益-402G、第2日維持費6G/利益-6G/前日比396G。読込後7checkpointとpreclose/close全9varが600568bytesでliteral一致。実PNGとpure observer/strict native/正式保存handoff不変を同1名が独立レビュー。fixtureの元本額は本番融資条件ではなく、自然借入・税利息政策/producer・表裏資金・全費目3日受入へ換算しない。製品変更0/候補未採用/追加全回帰0/正式完了追加0。" "表金/裏金の初期5000G・旧Gold割当、費目別支払元・不足時規約を確定し、記帳/保存移行/通常UIを接続する。",
"D04の税ランク別額または算式・精算時点、利息算式/対象/精算時点等の必要条件を確定し、実producer/分類/支払契約/保存再開を実装する。",
"全費目/全資金区分の3営業日全資金増減・台帳照合、元本売上外、適用する不足/失敗/再送境界を代表シナリオで検証する。",
"借入あり2締め2施設の通常Viewport/折返し/スクロール/元本操作部は有限確認済み。関連金融変更を固めた正確な最終候補で必須全回帰と独立採用/全条件受入を行い正式台帳へ反映する。"
],
"updated_at_jst": "2026-10-19T00:54:23.322453+09:00"
},
"completed_at_jst": "2026-10-23T22:51:54.402572+09:00",
"completion_acceptance": {
"S": "895491456477b42a74e59fa239bea86c4fb9aa33",
"Q": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"M": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"repairs": 3,
"all4_normal_gates_passed": true,
"acceptance_ids": [
"P05-02-F1",
"P05-02-F2",
"P05-02-F3",
"P05-02-F4",
"P05-02-F5",
"P05-02-F6",
"P05-02-F7",
"P05-02-F8"
],
"independent_review": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_INDEPENDENT_REVIEW_8954914.json",
"bytes": 10653,
"sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5"
},
"formal_proof": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_FORMAL_COMPLETION_CB183FD.json",
"bytes": 30318,
"sha256": "27e197df6c636126ad74f0d3a30041134c4bd4a1c94415f9ab4c380195bf100e"
},
"acceptance_record": {
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_ACCEPTANCE_CB183FD.md",
"bytes": 5848,
"sha256": "98d9e202dcb575bb6882ff7abf73f152307ac606aa1a73b545c67f80d4f7cdde"
},
"review_high_medium": 0,
"review_low": 3,
"original_credit": 1
},
"residuals_outside_unit_acceptance": [
"P05 package stays in_progress: P05-03 planned; its three human questions are pending and no answer is inferred",
"Atomic back_gold through ordinary play not achieved: the F1 back-facility attendant is registered by register_staff because normal Main cannot hire service_attendant (P09/P11 remaining_paid_role, 2026-10-08)",
"Casino, extortion and unit-sale revenue sources absent (SPEC section 7 disclosure); fines and events pay from front funds only, disclosed without a dedicated test",
"Review low: supply panel mixes front-only on-hand with a front+back after-purchase figure (outside allowed_paths); deferred under the feature-first instruction",
"Review low: ledger panel labels front funds as on-hand; the ledger UI is the current mock to be replaced; deferred under the feature-first instruction",
"AR.staff.dismissal_severance (P05, D04) not addressed; human severance answer pending",
"Same Windows distribution device acceptance, long normal play and whole-game final acceptance remain"
]
}, },
{ {
"id": "P05-03", "id": "P05-03",
@@ -5681,7 +5753,7 @@
], ],
"cross_cutting_acceptance_scope": "Completion requirements only, not achieved results. Apply each to relevant slices; non-applicable views require a recorded reason.", "cross_cutting_acceptance_scope": "Completion requirements only, not achieved results. Apply each to relevant slices; non-applicable views require a recorded reason.",
"initial_revision_state": "in_progress", "initial_revision_state": "in_progress",
"updated_at_jst": "2026-10-10T14:11:25.401018+09:00", "updated_at_jst": "2026-10-23T22:51:54.402572+09:00",
"execution_progress": { "execution_progress": {
"current1285_candidate_checkpoint": { "current1285_candidate_checkpoint": {
"at_jst": "2026-10-07T23:54:31.996054+09:00", "at_jst": "2026-10-07T23:54:31.996054+09:00",
@@ -6245,7 +6317,8 @@
"whole_goal_accepted": false "whole_goal_accepted": false
}, },
"historical_current_summary_before_P05_01_reaudit_r01": "有料自動仕入れの既採用範囲を保持。読み取りコールバックが契約を破った場合の維持費2状態の復元漏れを補強し、同4case132/0と合成全165/13368で限定採用。全費目・税・借入・自然脅威・全P05は未完。 ミミ配置の新購入quoteと通常仕入れ入力の247機能+298parseを候補で限定照合。本体への今回反映、全費目/自然営業は未受入。 最新:検証済み18 source+outside1の計19 refsを本体source1285へ採用済み。全170/13961/0の実行ソースと全SHA一致。原本全受入は未完。 最新: 元本決算分類の現本体10/1失敗を再現。候補979/0・28恒久回帰を独立照合、本体採用0・全回帰/別PID保存未受入。全P05未完。 最新: 元本分類の再現1原因を修正。集中979/0、全172/14516/0を独立照合後4refs採用・28恒久回帰追加。この再現残件0、正式保存再開・本番融資操作は未受入、全P05未完。 最新: 元本4ケース・8正式保存を別PID再開し1825項目/0。全状態/通知/元本台帳・6再送noopを限定受入。本番銀行操作と全P05は未完。 最新: 既存負債の元本返済APIの欠落を修正。集中1064/0、全172/14540/0を独立照合後2refs採用・24恒久回帰追加。この再現残件0、新APIの正式保存再開・本番銀行操作は未受入、全P05未完。 最新: 元本返済API4ケース・8正式保存を別PID再開し1825項目/0。全状態/通知/元本台帳・6再送noopを限定受入。元本返済APIは限定受入。本番銀行操作と全P05は未完。 最新: 既存負債の返済を通常コマンドへ接続。集中1113/0・全172/14567/0を独立照合後2refs採用・27恒久回帰追加。この再現残件0、新コマンドの正式保存再開と本番銀行UIは未受入、全P05未完。 最新: 通常返済commandの5ケース・10正式保存を別PID再開2281/0で限定受入。原本3.4と9.2に対する元本返済経費の除外差異1を実Writer6日で確認し未修正。決算式・赤字判定と旧v4移行を通常UIより先に修正する。全P05未完。 最新: 実Runtime36項目/10失敗で元本返済経費の除外、3連続赤字の未発生、終了未移行、時間/操作の継続を再現。既知原因1未修正。原本決算と旧v4移行を通常UIより先に修正する。 最新: 原本の返済経費・3連続赤字・終了を集中1108/0、実旧v4の10正式保存移行と旧赤字回数の単体境界で検証。全172/14580/0後5refs採用・13恒久回帰追加。この既知原因残件0。Ops保存v5、新v5の別PID保存再開は未受入、全P05未完。 最新: 新v5の12正式保存・48memberを別PID再開2552/0で限定受入。実旧v4からの混在6日履歴、返済経費・3連続赤字・終了、10組の全状態不変を確認。全P05未完。 最新: 既存元本返済UIの集中535/0・全172/14626/0を照合し6参照を採用。融資条件・OS・Windows・全P05は未受入。 最新: 通常Mainの元本返済から原本3連続赤字終了まで進め、別PIDの2正式保存、15全状態、3終了後全状態不変を71/0で限定受入。本体変更0、全P05は未完。", "historical_current_summary_before_P05_01_reaudit_r01": "有料自動仕入れの既採用範囲を保持。読み取りコールバックが契約を破った場合の維持費2状態の復元漏れを補強し、同4case132/0と合成全165/13368で限定採用。全費目・税・借入・自然脅威・全P05は未完。 ミミ配置の新購入quoteと通常仕入れ入力の247機能+298parseを候補で限定照合。本体への今回反映、全費目/自然営業は未受入。 最新:検証済み18 source+outside1の計19 refsを本体source1285へ採用済み。全170/13961/0の実行ソースと全SHA一致。原本全受入は未完。 最新: 元本決算分類の現本体10/1失敗を再現。候補979/0・28恒久回帰を独立照合、本体採用0・全回帰/別PID保存未受入。全P05未完。 最新: 元本分類の再現1原因を修正。集中979/0、全172/14516/0を独立照合後4refs採用・28恒久回帰追加。この再現残件0、正式保存再開・本番融資操作は未受入、全P05未完。 最新: 元本4ケース・8正式保存を別PID再開し1825項目/0。全状態/通知/元本台帳・6再送noopを限定受入。本番銀行操作と全P05は未完。 最新: 既存負債の元本返済APIの欠落を修正。集中1064/0、全172/14540/0を独立照合後2refs採用・24恒久回帰追加。この再現残件0、新APIの正式保存再開・本番銀行操作は未受入、全P05未完。 最新: 元本返済API4ケース・8正式保存を別PID再開し1825項目/0。全状態/通知/元本台帳・6再送noopを限定受入。元本返済APIは限定受入。本番銀行操作と全P05は未完。 最新: 既存負債の返済を通常コマンドへ接続。集中1113/0・全172/14567/0を独立照合後2refs採用・27恒久回帰追加。この再現残件0、新コマンドの正式保存再開と本番銀行UIは未受入、全P05未完。 最新: 通常返済commandの5ケース・10正式保存を別PID再開2281/0で限定受入。原本3.4と9.2に対する元本返済経費の除外差異1を実Writer6日で確認し未修正。決算式・赤字判定と旧v4移行を通常UIより先に修正する。全P05未完。 最新: 実Runtime36項目/10失敗で元本返済経費の除外、3連続赤字の未発生、終了未移行、時間/操作の継続を再現。既知原因1未修正。原本決算と旧v4移行を通常UIより先に修正する。 最新: 原本の返済経費・3連続赤字・終了を集中1108/0、実旧v4の10正式保存移行と旧赤字回数の単体境界で検証。全172/14580/0後5refs採用・13恒久回帰追加。この既知原因残件0。Ops保存v5、新v5の別PID保存再開は未受入、全P05未完。 最新: 新v5の12正式保存・48memberを別PID再開2552/0で限定受入。実旧v4からの混在6日履歴、返済経費・3連続赤字・終了、10組の全状態不変を確認。全P05未完。 最新: 既存元本返済UIの集中535/0・全172/14626/0を照合し6参照を採用。融資条件・OS・Windows・全P05は未受入。 最新: 通常Mainの元本返済から原本3連続赤字終了まで進め、別PIDの2正式保存、15全状態、3終了後全状態不変を71/0で限定受入。本体変更0、全P05は未完。",
"historical_current_summary_before_P04_closure": "P05-01「自動仕入れ候補の受入」は原7条件と適用横断6条件を同1名が全受入。原work1394→1396は履歴として一字不変、現採用1299/source df8328eaへ証拠を拘束。見積・数量・通常単価・ミミ20%割引/解除後価格・凍結領収書・配送・通常UIの現実装を再使用。通常OS有料倉庫、手動4/12G、自動不足6/18G、draftと受理済設定、確定済/pending/funds/quarantine/Mimiの正式別PID読込を確認。実有料cargo8の549分保存をWriter8413/Reader9945で全typed再開、5/40/120分の全文一致、554分に一回配送完了、550分の正当な不足8/24G補充一回を保持。返送・停止/復帰・破壊・隔離・再送を現full rawと不足局所証拠へ対応。局所電力は導出flag同分gateと解除後fulltickであり自然持続停電ではない。負の検証履歴を保持し、未説明状態差0、正式4members/圧縮payload/全生証拠を照合。製品編集0・重複全回帰0、現版必須全回帰を再利用。本成果単位の不足0、正式完了7件。部分点なし、全ゲーム/原子要件/完成素材/全人物/Windows受入へ拡張しない。 P05-02「全費目と資金区分」、P05-03「金庫と銀行の通常営業」は未完。P05 package全体はin_progressを維持。 P05-02を現1299/source df8328eaで着手。原work/acceptanceと88成果単位を維持し、現全回帰の財務10試験655/0の本文・raw・実PID・marker・終了・private profileをRootと同1名が読取照合して再使用。現版は単一PLAYER_CASHで表金/裏金未接続。税/利息はaggregate0のみでproducer/category mapping未実装、D04本番条件と資金割当/旧Gold移行/支払規約の回答待ち。旧v5正式再開証拠は現版との差12refsにより自動転用しない。既存維持費/仕入れ/給与/元本契約は再使用可能だが全費目3営業日統合受入は未完。各費目を毎日非ゼロ/部分払/未払にする追加条件は設けない。既存確定費目の個別表示1ref候補を準備、本体未採用。製品変更0・重複全回帰0・正式完了追加0。Windows実機で本単位全体をブロックしない。 表示候補UI1refは本体未採用のまま、現1299に候補SHA19145a61を限定適用した隔離stageでparse・公開関連2suite142/0を確認。表示専用fixture31/0は異なる4費目、旧summary欠損key、日選択/refresh/最新への復帰を確認し4表示操作の全Runtime varbytes前後一致。値は本番税利息政策/実producer/3営業日証拠へ換算しない。通常OS Main PID24687で受付500G/資材8建設、通常completion、00:00維持費発生2G/支払2G/未払0/利益-2/手持4498を実観測。1280x720の借入なし1施設1決算で表/裏の4費目・施設別表示が可視。1461 paused front→back→frontは表示層active_layerの一byte0→1以外全literal一致、表への復帰全一致、通常close前後全一致。externalOStraceはRootの実CUA呼出忠実転記、独立hardwarecaptureではない。先行QA不備の負記録保持。同1名が有限証拠を独立受入、借入あり長表示/スクロールの有限確認は後段を参照、統合最終版全回帰は未実行。製品変更0/重複全回帰0/正式完了追加0、未確定資金規約・D04/全費目3日受入を保持。 同一表示候補SHA19145a61の隔離stageで、既存公開APIを用いた明示fixtureの借入元本1000G/返済400Gと有償倉庫2施設を準備し、実際の2締めを経て正式4member保存。native PID28624 14/0、通常OS Main PID29022でmanual_ui読込、日付選択、元本の折返し、4費目、施設別2行、実帳簿スクロール、返済100G未送信入力、上端復帰を確認。現金4692G/残元本600G、第1日維持費2G/利益-402G、第2日維持費6G/利益-6G/前日比396G。読込後7checkpointとpreclose/close全9varが600568bytesでliteral一致。実PNGとpure observer/strict native/正式保存handoff不変を同1名が独立レビュー。fixtureの元本額は本番融資条件ではなく、自然借入・税利息政策/producer・表裏資金・全費目3日受入へ換算しない。製品変更0/候補未採用/追加全回帰0/正式完了追加0。" "historical_current_summary_before_P04_closure": "P05-01「自動仕入れ候補の受入」は原7条件と適用横断6条件を同1名が全受入。原work1394→1396は履歴として一字不変、現採用1299/source df8328eaへ証拠を拘束。見積・数量・通常単価・ミミ20%割引/解除後価格・凍結領収書・配送・通常UIの現実装を再使用。通常OS有料倉庫、手動4/12G、自動不足6/18G、draftと受理済設定、確定済/pending/funds/quarantine/Mimiの正式別PID読込を確認。実有料cargo8の549分保存をWriter8413/Reader9945で全typed再開、5/40/120分の全文一致、554分に一回配送完了、550分の正当な不足8/24G補充一回を保持。返送・停止/復帰・破壊・隔離・再送を現full rawと不足局所証拠へ対応。局所電力は導出flag同分gateと解除後fulltickであり自然持続停電ではない。負の検証履歴を保持し、未説明状態差0、正式4members/圧縮payload/全生証拠を照合。製品編集0・重複全回帰0、現版必須全回帰を再利用。本成果単位の不足0、正式完了7件。部分点なし、全ゲーム/原子要件/完成素材/全人物/Windows受入へ拡張しない。 P05-02「全費目と資金区分」、P05-03「金庫と銀行の通常営業」は未完。P05 package全体はin_progressを維持。 P05-02を現1299/source df8328eaで着手。原work/acceptanceと88成果単位を維持し、現全回帰の財務10試験655/0の本文・raw・実PID・marker・終了・private profileをRootと同1名が読取照合して再使用。現版は単一PLAYER_CASHで表金/裏金未接続。税/利息はaggregate0のみでproducer/category mapping未実装、D04本番条件と資金割当/旧Gold移行/支払規約の回答待ち。旧v5正式再開証拠は現版との差12refsにより自動転用しない。既存維持費/仕入れ/給与/元本契約は再使用可能だが全費目3営業日統合受入は未完。各費目を毎日非ゼロ/部分払/未払にする追加条件は設けない。既存確定費目の個別表示1ref候補を準備、本体未採用。製品変更0・重複全回帰0・正式完了追加0。Windows実機で本単位全体をブロックしない。 表示候補UI1refは本体未採用のまま、現1299に候補SHA19145a61を限定適用した隔離stageでparse・公開関連2suite142/0を確認。表示専用fixture31/0は異なる4費目、旧summary欠損key、日選択/refresh/最新への復帰を確認し4表示操作の全Runtime varbytes前後一致。値は本番税利息政策/実producer/3営業日証拠へ換算しない。通常OS Main PID24687で受付500G/資材8建設、通常completion、00:00維持費発生2G/支払2G/未払0/利益-2/手持4498を実観測。1280x720の借入なし1施設1決算で表/裏の4費目・施設別表示が可視。1461 paused front→back→frontは表示層active_layerの一byte0→1以外全literal一致、表への復帰全一致、通常close前後全一致。externalOStraceはRootの実CUA呼出忠実転記、独立hardwarecaptureではない。先行QA不備の負記録保持。同1名が有限証拠を独立受入、借入あり長表示/スクロールの有限確認は後段を参照、統合最終版全回帰は未実行。製品変更0/重複全回帰0/正式完了追加0、未確定資金規約・D04/全費目3日受入を保持。 同一表示候補SHA19145a61の隔離stageで、既存公開APIを用いた明示fixtureの借入元本1000G/返済400Gと有償倉庫2施設を準備し、実際の2締めを経て正式4member保存。native PID28624 14/0、通常OS Main PID29022でmanual_ui読込、日付選択、元本の折返し、4費目、施設別2行、実帳簿スクロール、返済100G未送信入力、上端復帰を確認。現金4692G/残元本600G、第1日維持費2G/利益-402G、第2日維持費6G/利益-6G/前日比396G。読込後7checkpointとpreclose/close全9varが600568bytesでliteral一致。実PNGとpure observer/strict native/正式保存handoff不変を同1名が独立レビュー。fixtureの元本額は本番融資条件ではなく、自然借入・税利息政策/producer・表裏資金・全費目3日受入へ換算しない。製品変更0/候補未採用/追加全回帰0/正式完了追加0。",
"historical_current_summary_before_P05_02_formal_cb183fd": "P05-01は正式受入済み、P05-02/03は未完。P04は通常候補b1810ff4で閉鎖し、原30/88の正式完了は17。次候補P05-02では全費目・表裏資金・3営業日照合・元本売上外を維持し、D04/資金配分・保存移行・支払規約の未確定条件と既存証拠の現版適用を実Opusが再監査して選定する。旧1299時点の候補・通常OS証拠は履歴として保全し、未検証の現版へ自動転用しない。新規実装は未起動。"
}, },
{ {
"id": "P06", "id": "P06",
@@ -65,6 +65,33 @@ func settle(period: Dictionary, economy: EconomyService) -> Dictionary:
return {&"ok": true, &"replayed": false, &"receipt": receipt.duplicate(true), &"transaction_id": transaction_id} return {&"ok": true, &"replayed": false, &"receipt": receipt.duplicate(true), &"transaction_id": transaction_id}
## Whole Gold that settling these closed periods would owe (arrears included),
## planned with no cash; -1 if a period is invalid or already settled. Nothing changes.
func preview_debt(periods: Array) -> int:
var arrears := _arrears.duplicate()
var fractions := _fractions.duplicate()
var last_end := int(_receipts[-1].period.period_end) if not _receipts.is_empty() else -1
var candidate := FacilityUpkeepSettlement.new()
for raw: Variant in periods:
if not raw is Dictionary:
return -1
var canonical := _parse_period(raw)
if canonical.is_empty() or (last_end >= 0 and canonical.period_start != last_end):
return -1
candidate._arrears = arrears
candidate._fractions = fractions
var plan := candidate._plan(canonical, 0)
if not plan[&"ok"]:
return -1
arrears = plan.arrears
fractions = plan.fractions
last_end = canonical.period_end
var debt := 0
for amount: int in arrears.values():
debt += amount
return debt
func get_arrears_through(minute: int) -> int: func get_arrears_through(minute: int) -> int:
var low := 0 var low := 0
var high := _receipts.size() var high := _receipts.size()
+86
View File
@@ -0,0 +1,86 @@
class_name FinancePolicy
extends RefCounted
## Front (表金) and hidden (裏金) funds, the World Tree rank tax and dark bank interest.
## Values are the user-approved initial test values (decisions A09-A12, 2026-10-23).
## The front fund keeps the existing PLAYER_CASH account; saves made before the split
## carry no hidden fund account and are read with every Gold as front funds (A12).
const FRONT_CASH: StringName = &"PLAYER_CASH"
const BACK_CASH: StringName = &"BACK_CASH"
const RANK_TAX_EXPENSE: StringName = &"RANK_TAX_EXPENSE"
const RANK_TAX_PAYABLE: StringName = &"RANK_TAX_PAYABLE"
const LOAN_INTEREST_EXPENSE: StringName = &"LOAN_INTEREST_EXPENSE"
const LOAN_INTEREST_PAYABLE: StringName = &"LOAN_INTEREST_PAYABLE"
const LOAN_LIABILITY_PREFIX := "LOAN_LIABILITY:"
## A10: 10% of the closed day's sales (front + hidden), whole Gold rounded down.
const RANK_TAX_PERCENT := 10
## A11: 1% of the outstanding principal per day, whole Gold rounded down.
const LOAN_INTEREST_PERCENT_PER_DAY := 1
## Services of facilities with one of these tags earn hidden funds (原本: 成人施設、
## カジノ、脅迫金、ユニット売却は裏の収益). Every other service earns front funds.
const BACK_FUND_TAGS: Array[StringName] = [&"restricted_service", &"adult_only"]
## Ledger categories. Accruals are expenses of the closed day; payments, top-ups and
## the payable balances are not income or expense.
const TOP_UP_CATEGORY: StringName = &"BACK_FUND_TOP_UP"
const RANK_TAX_CATEGORY: StringName = &"RANK_TAX"
const RANK_TAX_PAYMENT_CATEGORY: StringName = &"RANK_TAX_PAYMENT"
const LOAN_INTEREST_CATEGORY: StringName = &"LOAN_INTEREST"
const LOAN_INTEREST_PAYMENT_CATEGORY: StringName = &"LOAN_INTEREST_PAYMENT"
const NON_PROFIT_CATEGORIES: Array[StringName] = [TOP_UP_CATEGORY, RANK_TAX_PAYMENT_CATEGORY, LOAN_INTEREST_PAYMENT_CATEGORY]
static func rank_tax(sales: int) -> int:
return maxi(sales, 0) / 100 * RANK_TAX_PERCENT + maxi(sales, 0) % 100 * RANK_TAX_PERCENT / 100
static func loan_interest(outstanding_principal: int) -> int:
return maxi(outstanding_principal, 0) / 100 * LOAN_INTEREST_PERCENT_PER_DAY \
+ maxi(outstanding_principal, 0) % 100 * LOAN_INTEREST_PERCENT_PER_DAY / 100
static func is_back_fund_service(tags: Array) -> bool:
for tag: StringName in BACK_FUND_TAGS:
if tag in tags:
return true
return false
## Accounts of the split funds. A save without BACK_CASH predates the split and must
## not hold any of the other accounts either; they are then created empty (A12).
## Any other partial or incompatible set is rejected.
static func prepare_accounts(economy: EconomyService) -> bool:
if economy == null or not economy.has_account(FRONT_CASH) or economy.account_allows_negative(FRONT_CASH):
return false
var expected := {
BACK_CASH: false, RANK_TAX_EXPENSE: false, LOAN_INTEREST_EXPENSE: false,
RANK_TAX_PAYABLE: true, LOAN_INTEREST_PAYABLE: true,
}
if not economy.has_account(BACK_CASH):
for account_id: StringName in expected:
if economy.has_account(account_id):
return false
for account_id: StringName in expected:
if not economy.register_account(account_id, bool(expected[account_id])):
return false
return true
for account_id: StringName in expected:
if not economy.has_account(account_id) \
or economy.account_allows_negative(account_id) != bool(expected[account_id]):
return false
return true
static func outstanding_principal(economy: EconomyService) -> int:
var total := 0
for account: Dictionary in economy.get_accounts():
if String(account[&"account_id"]).begins_with(LOAN_LIABILITY_PREFIX):
total += -int(account[&"balance"])
return total
static func available_funds(economy: EconomyService) -> int:
return economy.get_balance(FRONT_CASH) + economy.get_balance(BACK_CASH)
+1
View File
@@ -0,0 +1 @@
uid://l14tq0avw0ux
+13 -4
View File
@@ -18,7 +18,8 @@ func preview_purchase(runtime: Variant, warehouse_id: StringName, item_id: Strin
or not bool(item.get(&"purchasable", false)) or quantity <= 0 or quantity > 10000: return quote or not bool(item.get(&"purchasable", false)) or quantity <= 0 or quantity > 10000: return quote
var frozen := PurchaseQuoteV2.make(item_id, quantity, int(item[&"unit_price"])) var frozen := PurchaseQuoteV2.make(item_id, quantity, int(item[&"unit_price"]))
if frozen.is_empty(): return quote if frozen.is_empty(): return quote
var cash: int = runtime.economy_service.get_balance(&"PLAYER_CASH") # A09: purchases may use the hidden fund for what the front fund cannot pay.
var cash: int = FinancePolicy.available_funds(runtime.economy_service)
var space: int = maxi(SupplyCatalog.storage_capacity(facility.definition_id) - SupplyCatalog.inventory_weight(runtime.inventory_service, facility.local_inventory_id) - _incoming_weight(runtime, facility.local_inventory_id), 0) var space: int = maxi(SupplyCatalog.storage_capacity(facility.definition_id) - SupplyCatalog.inventory_weight(runtime.inventory_service, facility.local_inventory_id) - _incoming_weight(runtime, facility.local_inventory_id), 0)
@warning_ignore("integer_division") @warning_ignore("integer_division")
var slots := space / int(item[&"unit_weight"]) var slots := space / int(item[&"unit_weight"])
@@ -40,7 +41,7 @@ func preview_purchase(runtime: Variant, warehouse_id: StringName, item_id: Strin
func _preview_purchase_v1(runtime: Variant, warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary: func _preview_purchase_v1(runtime: Variant, warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary:
var cash: int = runtime.economy_service.get_balance(&"PLAYER_CASH") var cash: int = FinancePolicy.available_funds(runtime.economy_service)
var item := SupplyCatalog.get_item(item_id) var item := SupplyCatalog.get_item(item_id)
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id) var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
var quote := {&"ok": false, &"reason": &"", &"total_cost": 0, &"cash_after": cash, &"quantity_after": 0, &"max_quantity": 0} var quote := {&"ok": false, &"reason": &"", &"total_cost": 0, &"cash_after": cash, &"quantity_after": 0, &"max_quantity": 0}
@@ -99,6 +100,13 @@ func _purchase_paid(runtime: Variant, warehouse_id: StringName, item_id: StringN
var memo: StringName = SupplyAutoPurchasePlans.receipt_memo(item_id, quantity, int(item[&"unit_price"]), int(quote[&"total_cost"])) if record_auto_attempt else &"supply_purchase" var memo: StringName = SupplyAutoPurchasePlans.receipt_memo(item_id, quantity, int(item[&"unit_price"]), int(quote[&"total_cost"])) if record_auto_attempt else &"supply_purchase"
if quote.has(&"purchase_quote"): if quote.has(&"purchase_quote"):
memo = PurchaseQuoteV2.receipt_memo(quote[&"purchase_quote"]) memo = PurchaseQuoteV2.receipt_memo(quote[&"purchase_quote"])
# A09: the hidden fund covers what the front fund cannot pay, in its own posting
# before the single-entry purchase (its receipt shape is unchanged).
var top_up: CommandResult = runtime.top_up_front_fund(int(quote[&"total_cost"]), minute, StringName("finance.back_top_up.purchase." + String(ledger_key)))
if not top_up.is_success():
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _purchase_failure(command_id, &"PAYMENT_FAILED")
var paid: CommandResult = runtime.economy_service.transfer(&"PLAYER_CASH", runtime.OPERATING_EXPENSE_ACCOUNT, int(quote[&"total_cost"]), minute, &"OPERATING_EXPENSE", warehouse_id, ledger_key, memo, &"", false) var paid: CommandResult = runtime.economy_service.transfer(&"PLAYER_CASH", runtime.OPERATING_EXPENSE_ACCOUNT, int(quote[&"total_cost"]), minute, &"OPERATING_EXPENSE", warehouse_id, ledger_key, memo, &"", false)
if not paid.is_success(): if not paid.is_success():
runtime.inventory_service.restore_snapshot(inventory_before) runtime.inventory_service.restore_snapshot(inventory_before)
@@ -114,7 +122,8 @@ func _purchase_paid(runtime: Variant, warehouse_id: StringName, item_id: StringN
runtime.inventory_service.restore_snapshot(inventory_before) runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before) runtime.economy_service.restore_snapshot(economy_before)
return _purchase_failure(command_id, &"PAYMENT_FAILED") return _purchase_failure(command_id, &"PAYMENT_FAILED")
if not runtime.economy_service.commit_transaction_notification(StringName(paid.details[&"transaction_id"])): if not runtime.economy_service.commit_transaction_notification(StringName(paid.details[&"transaction_id"])) \
or (int(top_up.details.get(&"amount", 0)) > 0 and not runtime.economy_service.commit_transaction_notification(StringName(top_up.details[&"transaction_id"]))):
runtime.inventory_service.restore_snapshot(inventory_before) runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before) runtime.economy_service.restore_snapshot(economy_before)
if record_auto_attempt: if record_auto_attempt:
@@ -274,7 +283,7 @@ func get_report(runtime: Variant) -> Dictionary:
row[&"reason_codes"].append(&"NO_INPUT") row[&"reason_codes"].append(&"NO_INPUT")
destinations.append(row) destinations.append(row)
var bridge_report: Dictionary = runtime.supply_logistics.get_report() var bridge_report: Dictionary = runtime.supply_logistics.get_report()
return {&"cash": runtime.economy_service.get_balance(&"PLAYER_CASH"), &"catalog": SupplyCatalog.get_purchase_items(), &"warehouses": warehouses, &"kitchens": kitchens, &"destinations": destinations, &"deliveries": bridge_report.get(&"deliveries", [])} return {&"cash": runtime.economy_service.get_balance(&"PLAYER_CASH"), &"back_cash": runtime.economy_service.get_balance(FinancePolicy.BACK_CASH), &"catalog": SupplyCatalog.get_purchase_items(), &"warehouses": warehouses, &"kitchens": kitchens, &"destinations": destinations, &"deliveries": bridge_report.get(&"deliveries", [])}
func ensure_deliveries(runtime: Variant, minute: int) -> void: func ensure_deliveries(runtime: Variant, minute: int) -> void:
+37 -11
View File
@@ -76,9 +76,17 @@ func get_report() -> Dictionary:
var construction_outflow := 0 var construction_outflow := 0
var wage_outflow := 0 var wage_outflow := 0
var upkeep_outflow := 0 var upkeep_outflow := 0
var back_inflow := 0
var back_top_up := 0
var rank_tax_outflow := 0
var loan_interest_outflow := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries(): for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.absolute_minute < start or entry.absolute_minute > minute or entry.category == &"INITIALIZATION": if entry.absolute_minute < start or entry.absolute_minute > minute or entry.category == &"INITIALIZATION":
continue continue
if entry.debit_account == FinancePolicy.BACK_CASH:
back_inflow += entry.amount
if entry.credit_account == FinancePolicy.BACK_CASH and entry.category == FinancePolicy.TOP_UP_CATEGORY:
back_top_up += entry.amount
if entry.debit_account == &"PLAYER_CASH": if entry.debit_account == &"PLAYER_CASH":
inflow += entry.amount inflow += entry.amount
if entry.credit_account == &"PLAYER_CASH": if entry.credit_account == &"PLAYER_CASH":
@@ -89,6 +97,10 @@ func get_report() -> Dictionary:
upkeep_outflow += entry.amount upkeep_outflow += entry.amount
elif entry.category == &"WAGE": elif entry.category == &"WAGE":
wage_outflow += entry.amount wage_outflow += entry.amount
elif entry.category == FinancePolicy.RANK_TAX_PAYMENT_CATEGORY:
rank_tax_outflow += entry.amount
elif entry.category == FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY:
loan_interest_outflow += entry.amount
var summaries: Array[Dictionary] = [] var summaries: Array[Dictionary] = []
if _runtime.operations_coordinator != null: if _runtime.operations_coordinator != null:
summaries = _runtime.operations_coordinator.get_day_summaries() summaries = _runtime.operations_coordinator.get_day_summaries()
@@ -105,6 +117,11 @@ func get_report() -> Dictionary:
&"construction_cash_out": construction_outflow, &"wage_cash_out": wage_outflow, &"construction_cash_out": construction_outflow, &"wage_cash_out": wage_outflow,
&"upkeep_cash_out": upkeep_outflow, &"upkeep_cash_out": upkeep_outflow,
&"upkeep_arrears": _runtime.upkeep_settlement.get_arrears_total(), &"upkeep_arrears": _runtime.upkeep_settlement.get_arrears_total(),
&"back_cash": _runtime.economy_service.get_balance(FinancePolicy.BACK_CASH),
&"back_cash_in": back_inflow, &"back_top_up": back_top_up,
&"rank_tax_cash_out": rank_tax_outflow, &"loan_interest_cash_out": loan_interest_outflow,
&"finance_arrears": -_runtime.economy_service.get_balance(FinancePolicy.RANK_TAX_PAYABLE)
- _runtime.economy_service.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE),
&"closed_days": summaries, &"selected_close": selected.duplicate(true), &"closed_days": summaries, &"selected_close": selected.duplicate(true),
} }
@@ -117,6 +134,8 @@ func refresh() -> void:
_refresh_repayment_controls() _refresh_repayment_controls()
var report := get_report() var report := get_report()
_cash_label.text = "第%d営業日 · 手持ち %dG\n本日の入金 +%dG / 出金 −%dG\nうち建築 %dG / 給与支払 %dG / 維持費支払 %dG\n未払い維持費 %dG\n資金増減 %+dG(営業利益は下の決算)" % [report[&"business_day"], report[&"cash"], report[&"cash_in"], report[&"cash_out"], report[&"construction_cash_out"], report[&"wage_cash_out"], report[&"upkeep_cash_out"], report[&"upkeep_arrears"], report[&"cash_net"]] _cash_label.text = "第%d営業日 · 手持ち %dG\n本日の入金 +%dG / 出金 −%dG\nうち建築 %dG / 給与支払 %dG / 維持費支払 %dG\n未払い維持費 %dG\n資金増減 %+dG(営業利益は下の決算)" % [report[&"business_day"], report[&"cash"], report[&"cash_in"], report[&"cash_out"], report[&"construction_cash_out"], report[&"wage_cash_out"], report[&"upkeep_cash_out"], report[&"upkeep_arrears"], report[&"cash_net"]]
# 表金は上の手持ち。裏金は裏のサービスの売上で、表金で払えない分だけを補う(A09)。
_cash_label.text += "\n裏金 %dG(本日の入金 +%dG / 表金へ補填 −%dG)\n本日の税 %dG / 利息 %dG(未払い %dG)" % [report[&"back_cash"], report[&"back_cash_in"], report[&"back_top_up"], report[&"rank_tax_cash_out"], report[&"loan_interest_cash_out"], report[&"finance_arrears"]]
var summary: Dictionary = report[&"selected_close"] var summary: Dictionary = report[&"selected_close"]
if summary.is_empty(): if summary.is_empty():
if not _day_picker.disabled or _day_picker.item_count == 0: if not _day_picker.disabled or _day_picker.item_count == 0:
@@ -143,6 +162,11 @@ func refresh() -> void:
for key: StringName in [&"maintenance", &"utilities", &"taxes_and_interest", &"fines"]: for key: StringName in [&"maintenance", &"utilities", &"taxes_and_interest", &"fines"]:
other += int(totals.get(key, 0)) other += int(totals.get(key, 0))
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n日次利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"] _closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n日次利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
var funds: Dictionary = (summary.get(&"settlement", {}) as Dictionary).get(&"funds", {})
if not funds.is_empty():
# The day's every top-up; a report saved before the version mark counted the close's own only.
var top_up_scope := "" if funds.has(&"fund_report_version") or funds.has("fund_report_version") else "(締めの時の分のみ)"
_closed_label.text += "\nうち表の売上 %dG / 裏の売上 %dG\n世界樹ランク税 %dG(支払 %dG)/ 利息 %dG(支払 %dG)\n裏金から表金へ補填%s %dG / 未払い 税 %dG・利息 %dG" % [int(funds.get(&"revenue_front", 0)), int(funds.get(&"revenue_back", 0)), int(funds.get(&"rank_tax", 0)), int(funds.get(&"rank_tax_paid", 0)), int(funds.get(&"loan_interest", 0)), int(funds.get(&"loan_interest_paid", 0)), top_up_scope, int(funds.get(&"back_top_up", 0)), int(funds.get(&"rank_tax_arrears", 0)), int(funds.get(&"loan_interest_arrears", 0))]
if totals.has(&"financing_principal_inflows") or totals.has(&"financing_principal_outflows"): if totals.has(&"financing_principal_inflows") or totals.has(&"financing_principal_outflows"):
_closed_label.text += "\n借入元本の入金 %dG(売上外) / 元本返済 %dG(決算経費)" % [int(totals.get(&"financing_principal_inflows", 0)), int(totals.get(&"financing_principal_outflows", 0))] _closed_label.text += "\n借入元本の入金 %dG(売上外) / 元本返済 %dG(決算経費)" % [int(totals.get(&"financing_principal_inflows", 0)), int(totals.get(&"financing_principal_outflows", 0))]
if _runtime.operations_coordinator.is_legacy_principal_profit_history(int(summary[&"day"])): if _runtime.operations_coordinator.is_legacy_principal_profit_history(int(summary[&"day"])):
@@ -206,7 +230,7 @@ func _build_repayment_controls(style_button: Callable) -> void:
loan_amount = LineEdit.new() loan_amount = LineEdit.new()
loan_amount.name = "LoanRepaymentAmount" loan_amount.name = "LoanRepaymentAmount"
loan_amount.placeholder_text = "返済する元本(整数G)" loan_amount.placeholder_text = "返済する元本(整数G)"
loan_amount.tooltip_text = "残元本と手持ちを超えない整数額を入力" loan_amount.tooltip_text = "残元本と表金・裏金の合計を超えない整数額を入力"
_loan_box.add_child(loan_amount) _loan_box.add_child(loan_amount)
loan_amount.text_changed.connect(func(_text: String) -> void: _refresh_repayment_controls()) loan_amount.text_changed.connect(func(_text: String) -> void: _refresh_repayment_controls())
loan_amount.text_submitted.connect(func(_text: String) -> void: submit_repayment(false)) loan_amount.text_submitted.connect(func(_text: String) -> void: submit_repayment(false))
@@ -265,12 +289,14 @@ func _refresh_repayment_controls() -> void:
var row := _loan_row(report) var row := _loan_row(report)
var remaining := int(row.get(&"outstanding_principal", 0)) var remaining := int(row.get(&"outstanding_principal", 0))
var cash := int(report.get(&"cash", 0)) var cash := int(report.get(&"cash", 0))
# A09: the front fund pays first and the hidden fund covers the rest in the same posting.
var funds := int(report.get(&"available_funds", cash))
var waiting := not (row.get(&"pending_command_ids", []) as Array).is_empty() var waiting := not (row.get(&"pending_command_ids", []) as Array).is_empty()
var allowed := bool(report.get(&"available", false)) and bool(report.get(&"ledger_valid", false)) and not bool(report.get(&"terminal", false)) and remaining > 0 and not waiting var allowed := bool(report.get(&"available", false)) and bool(report.get(&"ledger_valid", false)) and not bool(report.get(&"terminal", false)) and remaining > 0 and not waiting
var text := loan_amount.text.strip_edges() var text := loan_amount.text.strip_edges()
var amount := int(text) if text.is_valid_int() else 0 var amount := int(text) if text.is_valid_int() else 0
repay_button.disabled = not allowed or amount <= 0 or amount > remaining or amount > cash repay_button.disabled = not allowed or amount <= 0 or amount > remaining or amount > funds
repay_all_button.disabled = not allowed or remaining > cash repay_all_button.disabled = not allowed or remaining > funds
loan_amount.editable = allowed loan_amount.editable = allowed
if not bool(report.get(&"available", false)) or not bool(report.get(&"ledger_valid", false)): if not bool(report.get(&"available", false)) or not bool(report.get(&"ledger_valid", false)):
_loan_hint.text = "返済の帳簿を確認できません。" _loan_hint.text = "返済の帳簿を確認できません。"
@@ -279,13 +305,13 @@ func _refresh_repayment_controls() -> void:
elif loans.is_empty(): elif loans.is_empty():
_loan_hint.text = "借入はありません。" _loan_hint.text = "借入はありません。"
elif waiting: elif waiting:
_loan_hint.text = "返済を受理済みです。次の時間更新で実行し、手持ちと残元本を再確認します。" _loan_hint.text = "返済を受理済みです。次の時間更新で実行し、表金・裏金と残元本を再確認します。"
elif remaining == 0: elif remaining == 0:
_loan_hint.text = "この借入は完済しています。" _loan_hint.text = "この借入は完済しています。"
else: else:
_loan_hint.text = "手持ち %dGから元本を返済します。残元本 %dG。返済額は日次経費に含まれます。" % [cash, remaining] _loan_hint.text = "表金 %dG・裏金 %dG(計 %dG)から元本を返済します。表金で足りない分は裏金から補填します。残元本 %dG。返済額は日次経費に含まれます。" % [cash, funds - cash, funds, remaining]
if remaining > cash: _loan_hint.text += "\n完済には手持ちが不足しています。" if remaining > funds: _loan_hint.text += "\n完済には表金と裏金の合計が不足しています。"
if not text.is_empty() and repay_button.disabled: _loan_hint.text += "\n残元本と手持ちを超えない正の整数額を入力してください。" if not text.is_empty() and repay_button.disabled: _loan_hint.text += "\n残元本と表金・裏金の合計を超えない正の整数額を入力してください。"
func submit_repayment(full: bool = false) -> void: func submit_repayment(full: bool = false) -> void:
@@ -294,8 +320,8 @@ func submit_repayment(full: bool = false) -> void:
var remaining := int(row.get(&"outstanding_principal", 0)) var remaining := int(row.get(&"outstanding_principal", 0))
var text := loan_amount.text.strip_edges() var text := loan_amount.text.strip_edges()
var amount := remaining if full else (int(text) if text.is_valid_int() else 0) var amount := remaining if full else (int(text) if text.is_valid_int() else 0)
if not bool(report.get(&"ledger_valid", false)) or bool(report.get(&"terminal", false)) or row.is_empty() or not (row.get(&"pending_command_ids", []) as Array).is_empty() or amount <= 0 or amount > remaining or amount > int(report.get(&"cash", 0)): if not bool(report.get(&"ledger_valid", false)) or bool(report.get(&"terminal", false)) or row.is_empty() or not (row.get(&"pending_command_ids", []) as Array).is_empty() or amount <= 0 or amount > remaining or amount > int(report.get(&"available_funds", report.get(&"cash", 0))):
_set_repayment_result("返済できません。手持ち・残元本・返済待ちを確認してください。", false) _set_repayment_result("返済できません。表金・裏金・残元本・返済待ちを確認してください。", false)
_refresh_repayment_controls() _refresh_repayment_controls()
return return
var key := StringName("ui.loan.repay.%d" % int(report[&"next_command_id"])) var key := StringName("ui.loan.repay.%d" % int(report[&"next_command_id"]))
@@ -308,7 +334,7 @@ func submit_repayment(full: bool = false) -> void:
var after := _loan_row(_loan_report()) var after := _loan_row(_loan_report())
if result.is_success() and (after.get(&"pending_command_ids", []) as Array).has(int(result.command_id)): if result.is_success() and (after.get(&"pending_command_ids", []) as Array).has(int(result.command_id)):
_loan_pending_command = int(result.command_id) _loan_pending_command = int(result.command_id)
_set_repayment_result("返済を受理しました。実行時に手持ちと残元本を確認します。", true) _set_repayment_result("返済を受理しました。実行時に表金・裏金と残元本を確認します。", true)
else: else:
_show_repayment_result(result) _show_repayment_result(result)
_refresh_repayment_controls() _refresh_repayment_controls()
@@ -319,7 +345,7 @@ func _show_repayment_result(result: Variant) -> void:
_set_repayment_result("元本 %dGを返済しました。残元本 %dG。" % [int(result.details.get(&"paid_principal", 0)), int(result.details.get(&"outstanding_principal", 0))], true) _set_repayment_result("元本 %dGを返済しました。残元本 %dG。" % [int(result.details.get(&"paid_principal", 0)), int(result.details.get(&"outstanding_principal", 0))], true)
loan_amount.text = "" loan_amount.text = ""
else: else:
_set_repayment_result("返済を実行できませんでした。手持ちと残元本を確認してください。", false) _set_repayment_result("返済を実行できませんでした。表金・裏金と残元本を確認してください。", false)
func _on_repayment_completed(result: Variant) -> void: func _on_repayment_completed(result: Variant) -> void:
+22
View File
@@ -300,6 +300,28 @@ func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> D
old[&"elevators"] = raised old[&"elevators"] = raised
fields.append("elevators_v1_to_v2") fields.append("elevators_v1_to_v2")
proof["elevators"] = {"only_version_changed": same_otherwise} proof["elevators"] = {"only_version_changed": same_otherwise}
# P05-02 A12: a save from before the funds were split gains the five empty fund
# accounts on load; every other economy field stays byte-identical.
var has_back_cash := func(economy: Variant) -> bool:
if not economy is Dictionary:
return false
for account: Variant in (economy as Dictionary).get(&"accounts", []):
if account is Dictionary and String((account as Dictionary).get(&"account_id", "")) == "BACK_CASH":
return true
return false
if not has_back_cash.call(old.get(&"economy")) and has_back_cash.call(now.get(&"economy")):
var added := {"BACK_CASH": false, "LOAN_INTEREST_EXPENSE": false, "LOAN_INTEREST_PAYABLE": true, "RANK_TAX_EXPENSE": false, "RANK_TAX_PAYABLE": true}
var economy: Dictionary = (old[&"economy"] as Dictionary).duplicate(true)
var accounts: Array = (economy[&"accounts"] as Array).duplicate(true)
for account_id: String in added:
accounts.append({&"account_id": account_id, &"allow_negative": added[account_id], &"system": false})
accounts.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"account_id"]) < String(right[&"account_id"]))
economy[&"accounts"] = accounts
var only_accounts_added: bool = var_to_bytes(economy) == var_to_bytes(now[&"economy"])
ok = ok and only_accounts_added
old[&"economy"] = economy
fields.append("economy_A12_empty_fund_accounts")
proof["economy_A12"] = {"added_accounts": added.keys(), "only_accounts_added": only_accounts_added}
if not fields.is_empty(): if not fields.is_empty():
# Saves keep the Runtime's own key order, so the mapped tree follows it. # Saves keep the Runtime's own key order, so the mapped tree follows it.
ok = ok and old.size() == now.size() ok = ok and old.size() == now.size()
@@ -0,0 +1,77 @@
extends SceneTree
## P05-02 F6, writer phase. Plays the day script, writes ordinary save files at
## mid-day, just before the day 2 00:00 close and just after it, and hands the
## exact save packages to the reader suite (test_p05_funds_restart_2_read.gd),
## which runs as a separate engine process. The handoff is the declared generated
## report res://artifacts/p05-02/restart-handoff.json
## (tools/regression/suite-policy.json fixed_generated_report_routes).
const Fixture := preload("res://tests/helpers/p05_funds_restart_fixture.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P05-02: restart writer")
var fixtures := Fixture.new(_expect)
var straight := fixtures.new_fixture()
fixtures.play(straight, 0, Fixture.LAST_MINUTE, Callable())
var straight_runtime: Variant = straight[&"runtime"]
_expect(straight_runtime.economy_service.get_balance(Fixture.FRONT) == 35 and straight_runtime.economy_service.get_balance(Fixture.BACK) == 695, "uninterrupted day script ends with front 35 and hidden 695")
_expect(fixtures.category_count(straight_runtime, [FinancePolicy.TOP_UP_CATEGORY]) == 1, "uninterrupted day script tops up once (day 2)")
var saving := fixtures.new_fixture()
var saved_paths: Dictionary = {}
fixtures.play(saving, 0, Fixture.LAST_MINUTE, func(minute: int) -> void:
if minute in Fixture.SAVE_MINUTES:
var path := Fixture.WRITER_SAVE % minute
_expect(bool(saving[&"runtime"].save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute)
saved_paths[minute] = path)
_expect(fixtures.economy_bytes(saving[&"runtime"]) == fixtures.economy_bytes(straight_runtime), "writing the save files leaves the ledger as without saving")
var saves: Array = []
for minute: int in Fixture.SAVE_MINUTES:
var directory := ProjectSettings.globalize_path(str(saved_paths.get(minute, Fixture.WRITER_SAVE % minute)))
_expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package at minute %d holds only files" % minute)
var files := {}
for name: String in DirAccess.get_files_at(directory):
var bytes := FileAccess.get_file_as_bytes(directory.path_join(name))
files[name] = {"sha256": Fixture.sha256(bytes), "bytes": bytes.size(), "base64": Marshalls.raw_to_base64(bytes)}
_expect(files.has("manifest.json") and files.has("world.bin"), "save package at minute %d has its manifest and world" % minute)
saves.append({"minute": minute, "files": files})
var handoff := {
"schema": Fixture.HANDOFF_SCHEMA,
"writer_pid": OS.get_process_id(),
"writer_source_sha256": FileAccess.get_sha256(Fixture.WRITER_SOURCE),
"seed": Fixture.SEED,
"last_minute": Fixture.LAST_MINUTE,
"straight_economy_sha256": Fixture.sha256(fixtures.economy_bytes(straight_runtime)),
"straight_days_base64": Marshalls.raw_to_base64(var_to_bytes(fixtures.days_value(straight_runtime))),
"saves": saves,
}
DirAccess.make_dir_recursive_absolute(Fixture.HANDOFF_PATH.get_base_dir())
var file := FileAccess.open(Fixture.HANDOFF_PATH, FileAccess.WRITE)
_expect(file != null, "handoff file opens for writing")
if file != null:
file.store_string(JSON.stringify(handoff, "\t") + "\n")
file.close()
print("P05_RESTART_WRITER pid=%d handoff_sha256=%s" % [OS.get_process_id(), FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
fixtures.free_fixture(straight)
fixtures.free_fixture(saving)
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
@@ -0,0 +1 @@
uid://bsck1wvucocy3
@@ -0,0 +1,119 @@
extends SceneTree
## P05-02 F6, reader phase. Runs after the writer suite
## (test_p05_funds_restart_1_write.gd) as a separate engine process, loads each
## save package the writer wrote, continues the day script and requires the same
## ledger and daily closes as the uninterrupted run: no double rank tax, interest
## or top-up across mid-day, pre-00:00 and post-00:00 restarts. A changed package
## is refused.
const Fixture := preload("res://tests/helpers/p05_funds_restart_fixture.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P05-02: restart reader")
var handoff := _read_handoff()
if not handoff.is_empty():
_check_restarts(handoff)
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _read_handoff() -> Dictionary:
_expect(FileAccess.file_exists(Fixture.HANDOFF_PATH), "the writer phase left its handoff (run test_p05_funds_restart_1_write.gd first)")
if not FileAccess.file_exists(Fixture.HANDOFF_PATH):
return {}
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(Fixture.HANDOFF_PATH))
_expect(parsed is Dictionary and str((parsed as Dictionary).get("schema", "")) == Fixture.HANDOFF_SCHEMA, "handoff schema")
if not parsed is Dictionary or str((parsed as Dictionary).get("schema", "")) != Fixture.HANDOFF_SCHEMA:
return {}
var handoff: Dictionary = parsed
var writer_pid := int(handoff.get("writer_pid", 0))
_expect(writer_pid > 0 and writer_pid != OS.get_process_id(), "a separate engine process wrote the saves")
_expect(str(handoff.get("writer_source_sha256", "")) == FileAccess.get_sha256(Fixture.WRITER_SOURCE), "the handoff comes from the current writer suite")
_expect(int(handoff.get("seed", -1)) == Fixture.SEED and int(handoff.get("last_minute", -1)) == Fixture.LAST_MINUTE, "same fixture and day script")
print("P05_RESTART_READER pid=%d writer_pid=%d handoff_sha256=%s" % [OS.get_process_id(), writer_pid, FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
return handoff
func _check_restarts(handoff: Dictionary) -> void:
var fixtures := Fixture.new(_expect)
var straight := fixtures.new_fixture()
fixtures.play(straight, 0, Fixture.LAST_MINUTE, Callable())
var straight_runtime: Variant = straight[&"runtime"]
var straight_economy := fixtures.economy_bytes(straight_runtime)
_expect(Fixture.sha256(straight_economy) == str(handoff.get("straight_economy_sha256", "")), "the uninterrupted day script gives the same ledger in both processes")
var writer_days: Variant = bytes_to_var(Marshalls.base64_to_raw(str(handoff.get("straight_days_base64", ""))))
_expect(fixtures.days_equal(straight_runtime, writer_days), "the uninterrupted day script gives the same daily closes in both processes")
var saves: Array = handoff.get("saves", [])
var minutes: Array[int] = []
for save: Variant in saves:
minutes.append(int((save as Dictionary).get("minute", -1)) if save is Dictionary else -1)
_expect(minutes == Fixture.SAVE_MINUTES, "the writer saved mid-day, before and after the day 2 00:00 close")
for save: Variant in saves:
if not save is Dictionary:
continue
var minute := int((save as Dictionary).get("minute", -1))
var path := Fixture.READER_SAVE % minute
if not _materialize(path, (save as Dictionary).get("files", {}), false):
continue
var resumed := fixtures.new_fixture()
var runtime: Variant = resumed[&"runtime"]
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "this process loads the save written at minute %d" % minute)
_expect(int(resumed[&"clock"].absolute_minute) == minute, "the loaded save resumes at minute %d" % minute)
fixtures.play(resumed, minute, Fixture.LAST_MINUTE, Callable())
_expect(fixtures.economy_bytes(runtime) == straight_economy, "resuming at minute %d gives exactly the uninterrupted ledger" % minute)
_expect(fixtures.days_equal(runtime, writer_days), "resuming at minute %d gives the same daily closes, fund reports and arrears" % minute)
# Day 1: tax and interest; day 2: interest; day 3: tax and interest.
_expect(fixtures.category_count(runtime, [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]) == 5, "minute %d: each tax and interest accrual is posted once" % minute)
_expect(fixtures.category_count(runtime, [FinancePolicy.TOP_UP_CATEGORY]) == 1, "minute %d: the day 2 top-up is posted once" % minute)
_expect(runtime.economy_service.get_balance(Fixture.FRONT) == 35 and runtime.economy_service.get_balance(Fixture.BACK) == 695, "minute %d: front 35 and hidden 695" % minute)
fixtures.free_fixture(resumed)
if not saves.is_empty() and saves[0] is Dictionary and _materialize(Fixture.READER_TAMPERED_SAVE, (saves[0] as Dictionary).get("files", {}), true):
var fixture := fixtures.new_fixture()
var runtime: Variant = fixture[&"runtime"]
var before := fixtures.economy_bytes(runtime)
_expect(not bool(runtime.load_from_path(Fixture.READER_TAMPERED_SAVE).get(&"ok", true)), "a changed save package is refused")
_expect(fixtures.economy_bytes(runtime) == before and int(fixture[&"clock"].absolute_minute) == 360, "a refused load changes no Gold and no time")
fixtures.free_fixture(fixture)
fixtures.free_fixture(straight)
# Writes the writer's exact package bytes into this process's own profile.
func _materialize(path: String, files: Variant, change_world: bool) -> bool:
var directory := ProjectSettings.globalize_path(path)
_expect(files is Dictionary and (files as Dictionary).has("manifest.json") and (files as Dictionary).has("world.bin"), "handoff package %s is complete" % path)
if not files is Dictionary:
return false
_expect(DirAccess.make_dir_recursive_absolute(directory) == OK, "package directory %s" % path)
for name: String in (files as Dictionary):
var pin: Dictionary = (files as Dictionary)[name]
var bytes := Marshalls.base64_to_raw(str(pin.get("base64", "")))
_expect(Fixture.sha256(bytes) == str(pin.get("sha256", "")) and bytes.size() == int(pin.get("bytes", -1)), "handoff bytes of %s are exact" % name)
if change_world and name == "world.bin" and not bytes.is_empty():
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0xff
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
_expect(file != null, "write %s into %s" % [name, path])
if file == null:
return false
file.store_buffer(bytes)
file.close()
return true
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
@@ -0,0 +1 @@
uid://c8yrv3udeb87l
+825
View File
@@ -0,0 +1,825 @@
extends SceneTree
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
## P05-02: front (表金) and hidden (裏金) funds, the A09 payment rule, the A10 rank
## tax, the A11 loan interest, the three business day reconciliation and saves.
## The policy and reconciliation scenes use explicit ledger fixtures for sales,
## debt and drains, with the production Operations closes. The repair 2 scenes
## run the production paths themselves: built and staffed facilities whose guests
## complete their services, the automatic purchase, the repayment command and the
## legacy 00:00 upkeep; a fixture there only sets a balance (a loan, a drain), never
## the payment under test. The last check opens the ledger from the game's own Main
## (F7), so the runner gives this suite the ordinary Main audio cleanup proof
## (tools/regression/suite-policy.json).
const FRONT := &"PLAYER_CASH"
const BACK := &"BACK_CASH"
const LOAN := &"LOAN_LIABILITY:p05.fixture"
const FRONT_SALES := &"P05_FIXTURE_FRONT_SALES"
const BACK_SALES := &"P05_FIXTURE_BACK_SALES"
const PAYER := &"P05_FIXTURE_PAYER"
const SINK := &"P05_FIXTURE_SINK"
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P05-02: front and hidden funds")
_test_policy_values()
_test_sale_fund_routing()
_test_three_business_days()
_test_resume_matches_uninterrupted()
_test_old_save_migration()
_test_repayment_top_up()
_test_purchase_top_up()
_test_upkeep_top_up()
_test_actual_service_funds()
_test_operation_top_ups_and_day_total()
_test_legacy_midnight_top_up()
_test_tax_arrears_next_day()
_test_ledger_repayment_uses_hidden_funds()
await _test_ledger_opened_from_main()
await process_frame
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _new_fixture(seed: int) -> Dictionary:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect(bool(runtime.initialize(clock, state, seed).get(&"valid", false)), "fixture Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER, SINK]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
# Guests' Gold enters the ledger only through explicit initialization.
_expect(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "fixture payer funded")
return {&"clock": clock, &"state": state, &"runtime": runtime}
func _free_fixture(fixture: Dictionary) -> void:
for key: StringName in [&"runtime", &"clock", &"state"]:
if fixture.get(key) != null:
fixture[key].free()
# Same accelerated boundary traversal as tests/mvp/test_runtime_operations_acceptance.gd:
# the fixtures hold no Facilities, Staff or logistics, only ledger postings, which
# are made on 30-minute marks.
func _advance(fixture: Dictionary, amount: int) -> void:
var clock: Variant = fixture[&"clock"]
var runtime: Variant = fixture[&"runtime"]
var advanced := 0
while advanced < amount:
# Every Operations boundary is on a 30-minute mark; each step ends on one.
var step := mini(30 - int(clock.absolute_minute) % 30, amount - advanced)
var target: int = clock.absolute_minute + step
clock.absolute_minute = target
clock.call("_sync_calendar_fields")
clock.snapshot_version += step
runtime.call("_advance_weather", target)
runtime.call("_advance_facility_upkeep", target)
runtime.call("_advance_operations_time", target)
_align_clocks(runtime, target)
advanced += step
# Same as tests/mvp/test_runtime_operations_acceptance.gd: the fixture owns no
# actors, cars or rides, so only the empty aggregates' clocks follow the minute.
func _align_clocks(runtime: Variant, minute: int) -> void:
for service: Variant in [runtime.inventory_service, runtime.facility_service, runtime.kitchen_production,
runtime.guest_service, runtime.staff_service, runtime.authority_service, runtime.defense_service,
runtime.construction_service, runtime.excavation_service]:
service.current_minute = minute
var transport_snapshot: Dictionary = runtime.elevators.create_snapshot()
if not (transport_snapshot[&"shafts"] as Array).is_empty() or not (transport_snapshot[&"trips"] as Array).is_empty():
_expect(false, "accelerated fixture cannot skip live elevator movement")
return
transport_snapshot[&"current_minute"] = minute
if not runtime.elevators.restore_snapshot(transport_snapshot):
_expect(false, "empty transport clock aligns through validated snapshot restore")
func _advance_to(fixture: Dictionary, minute: int) -> void:
_advance(fixture, minute - int(fixture[&"clock"].absolute_minute))
func _sale(fixture: Dictionary, revenue_account: StringName, fund: StringName, amount: int, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
var minute: int = fixture[&"clock"].absolute_minute
var entries: Array = [
LedgerEntry.new().configure(&"", minute, revenue_account, PAYER, amount, &"SERVICE_SALE", &"p05.fixture"),
LedgerEntry.new().configure(&"", minute, fund, revenue_account, amount, &"SERVICE_CASH_SWEEP", &"p05.fixture"),
]
return runtime.economy_service.post(entries, StringName("p05.sale." + key)).is_success()
func _move(fixture: Dictionary, from: StringName, to: StringName, amount: int, category: StringName, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
return runtime.economy_service.transfer(from, to, amount, fixture[&"clock"].absolute_minute, category,
&"p05.fixture", StringName("p05.move." + key)).is_success()
func _fund_delta(runtime: Variant, account: StringName, after_minute: int, through_minute: int) -> int:
var delta := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.absolute_minute <= after_minute or entry.absolute_minute > through_minute:
continue
if entry.debit_account == account:
delta += entry.amount
if entry.credit_account == account:
delta -= entry.amount
return delta
func _test_policy_values() -> void:
_expect(FinancePolicy.rank_tax(800) == 80 and FinancePolicy.rank_tax(109) == 10 and FinancePolicy.rank_tax(9) == 0, "A10 rank tax is 10% rounded down")
_expect(FinancePolicy.rank_tax(-5) == 0, "no tax on a negative day")
_expect(FinancePolicy.loan_interest(1000) == 10 and FinancePolicy.loan_interest(199) == 1 and FinancePolicy.loan_interest(99) == 0, "A11 interest is 1% per day rounded down")
func _test_sale_fund_routing() -> void:
var fixture := _new_fixture(5021)
var runtime: Variant = fixture[&"runtime"]
var back_ids: Array[StringName] = []
var front_ids: Array[StringName] = []
# Expansion definitions are read from their files; they are not registered by default.
for path: String in ["res://data/facilities/adult_private_room.tres", "res://data/facilities/expansion/black_market.tres",
"res://data/facilities/expansion/vip_private_salon.tres", "res://data/facilities/cafeteria.tres"]:
var definition: Variant = load(path)
if definition == null:
continue
if FinancePolicy.is_back_fund_service(definition.tags):
back_ids.append(definition.id)
else:
front_ids.append(definition.id)
_expect(back_ids.has(&"adult_private_room") and back_ids.has(&"black_market") and back_ids.has(&"vip_private_salon"), "adult, trade and private salon services earn hidden funds")
_expect(front_ids.has(&"cafeteria"), "dining earns front funds")
_expect(runtime.economy_service.has_account(BACK) and not runtime.economy_service.account_allows_negative(BACK), "the hidden fund is its own non-negative account")
_expect(runtime.economy_service.get_balance(BACK) == 0 and runtime.economy_service.get_balance(FRONT) == 5000, "a new game starts with every Gold in front funds")
_free_fixture(fixture)
func _test_three_business_days() -> void:
var fixture := _new_fixture(5022)
var runtime: Variant = fixture[&"runtime"]
var state: Variant = fixture[&"state"]
var coordinator: Variant = runtime.operations_coordinator
# Day 1: borrow 1000, front sale 300, hidden sale 500.
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow"), "fixture principal borrowed")
_expect(_sale(fixture, FRONT_SALES, FRONT, 300, "d1.front") and _sale(fixture, BACK_SALES, BACK, 500, "d1.back"), "day 1 sales")
_advance_to(fixture, 1440)
var day1: Dictionary = coordinator.get_day_summary(1)
_expect(not day1.is_empty(), "day 1 closes at 00:00")
var totals1: Dictionary = day1.get(&"category_totals", {})
_expect(int(totals1.get(&"revenue", -1)) == 800, "day 1 sales are front + hidden")
_expect(int(totals1.get(&"taxes_and_interest", -1)) == 90, "day 1 rank tax 80 and interest 10")
_expect(int(totals1.get(&"financing_principal_inflows", -1)) == 1000, "principal stays financing, outside sales")
var funds1: Dictionary = day1.get(&"settlement", {}).get(&"funds", {})
_expect(int(funds1.get(&"revenue_front", -1)) == 300 and int(funds1.get(&"revenue_back", -1)) == 500, "day 1 fund report splits sales")
_expect(int(funds1.get(&"rank_tax", -1)) == 80 and int(funds1.get(&"rank_tax_paid", -1)) == 80, "day 1 rank tax paid in full")
_expect(int(funds1.get(&"loan_interest", -1)) == 10 and int(funds1.get(&"loan_interest_paid", -1)) == 10, "day 1 interest paid in full")
_expect(int(funds1.get(&"back_top_up", -1)) == 0, "front funds alone paid day 1")
_expect(runtime.economy_service.get_balance(FRONT) == 6210 and runtime.economy_service.get_balance(BACK) == 500, "day 1 balances")
_expect(state.get_resource(&"gold") == runtime.economy_service.get_balance(FRONT), "Gold mirrors front funds")
# The starting Gold is posted at 06:00 (minute 360); day 1 changes follow it.
_expect(_fund_delta(runtime, FRONT, 360, 1440) == 1210 and _fund_delta(runtime, BACK, 360, 1440) == 500, "day 1 fund changes equal the ledger")
# Day 2: front funds fall to 5, so 5 of the 10 interest comes from hidden funds.
_advance_to(fixture, 1440 + 600)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", 6205, &"OPERATING_EXPENSE", "d2.cost"), "day 2 purchase-like cost")
_advance_to(fixture, 2880)
var funds2: Dictionary = coordinator.get_day_summary(2).get(&"settlement", {}).get(&"funds", {})
_expect(int(funds2.get(&"back_top_up", -1)) == 5, "A09: hidden funds cover the 5 front funds cannot pay")
_expect(int(funds2.get(&"loan_interest_paid", -1)) == 10 and int(funds2.get(&"rank_tax", -1)) == 0, "day 2 interest paid, no sales no tax")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 495, "day 2 balances")
_expect(int(coordinator.get_day_summary(2)[&"category_totals"][&"revenue"]) == 0, "the top-up is not sales")
_expect(_fund_delta(runtime, FRONT, 1440, 2880) == -6210 and _fund_delta(runtime, BACK, 1440, 2880) == -5, "day 2 fund changes equal the ledger")
# Day 3: hidden funds are gone too, so the interest stays unpaid.
_advance_to(fixture, 2880 + 600)
_expect(_move(fixture, BACK, SINK, 495, &"TRANSFER", "d3.drain"), "day 3 hidden funds drained")
_advance_to(fixture, 4320)
var day3: Dictionary = coordinator.get_day_summary(3)
var funds3: Dictionary = day3.get(&"settlement", {}).get(&"funds", {})
_expect(int(funds3.get(&"loan_interest", -1)) == 10 and int(funds3.get(&"loan_interest_paid", -1)) == 0 and int(funds3.get(&"loan_interest_arrears", -1)) == 10, "A09: unpaid interest carries over")
_expect(int(day3[&"settlement"][&"arrears_total"]) == 10, "day 3 arrears include the interest")
_expect(int(runtime.get_operations_report().get(&"arrears_total", -1)) == 10, "Operations report sees the interest arrears")
_expect(int(day3[&"category_totals"][&"taxes_and_interest"]) == 10, "the unpaid interest is still the day's expense")
# Day 4: a front sale pays the older interest first.
_advance_to(fixture, 4320 + 600)
_expect(_sale(fixture, FRONT_SALES, FRONT, 100, "d4.front"), "day 4 sale")
_advance_to(fixture, 5760)
var funds4: Dictionary = coordinator.get_day_summary(4).get(&"settlement", {}).get(&"funds", {})
_expect(int(funds4.get(&"rank_tax", -1)) == 10 and int(funds4.get(&"rank_tax_paid", -1)) == 10, "day 4 tax paid")
_expect(int(funds4.get(&"loan_interest_paid", -1)) == 20 and int(funds4.get(&"loan_interest_arrears", -1)) == 0, "day 4 pays the older interest and the new one")
_expect(runtime.economy_service.get_balance(FRONT) == 70, "day 4 front balance")
_expect(runtime.economy_service.validate_ledger()[&"ok"], "the ledger reconciles after four closes")
_check_ledger_panel(runtime, fixture[&"clock"])
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "four closed days restore exactly")
_free_fixture(fixture)
func _scripted_days(fixture: Dictionary, restore_at: Array[int], through_files := false) -> Dictionary:
var runtime: Variant = fixture[&"runtime"]
# Postings and save/restore points in time order; the last minute closes day 3.
var steps: Array = [
[420, func() -> void: _move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow")],
[450, func() -> void: _sale(fixture, BACK_SALES, BACK, 700, "d1.back")],
[1440, Callable()],
[2040, func() -> void: _move(fixture, FRONT, &"OPERATING_EXPENSE", 5900, &"OPERATING_EXPENSE", "d2.cost")],
[2400, Callable()],
[2880, Callable()],
[3510, func() -> void: _sale(fixture, FRONT_SALES, FRONT, 50, "d3.front")],
[4320, Callable()],
]
for step: Array in steps:
var minute := int(step[0])
_advance_to(fixture, minute)
if (step[1] as Callable).is_valid():
(step[1] as Callable).call()
if minute in restore_at and through_files:
# The ordinary save file path: write the save, then load it back.
var path := "user://p05_resume_%d.save" % minute
_expect(bool(runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute)
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "load the save file at minute %d" % minute)
elif minute in restore_at:
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved), "restore at minute %d" % minute)
return runtime.create_snapshot()
func _test_resume_matches_uninterrupted() -> void:
var straight := _new_fixture(5023)
var resumed := _new_fixture(5023)
var a := _scripted_days(straight, [])
var b := _scripted_days(resumed, [1440, 2400, 2880])
_expect(var_to_bytes(a) == var_to_bytes(b), "saving mid-day and at 00:00 changes nothing: no double tax, interest or top-up")
var runtime: Variant = resumed[&"runtime"]
var keys := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category in [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]:
keys += 1
# Day 1: tax 70 and interest 10; day 2: interest only (no sales); day 3: tax 5 and interest 10.
_expect(keys == 5, "each tax and interest accrual is posted once (no duplicates)")
# Through save files the save and load events are new, so compare the funds side only.
var through_files := _new_fixture(5023)
var c := _scripted_days(through_files, [1440, 2400, 2880], true)
_expect(var_to_bytes(c[&"systems"][&"runtime"][&"economy"]) == var_to_bytes(a[&"systems"][&"runtime"][&"economy"]), "saving to files and loading them back leaves the ledger exactly as without saving")
var straight_days: Array = straight[&"runtime"].operations_coordinator.get_day_summaries()
var file_days: Array = through_files[&"runtime"].operations_coordinator.get_day_summaries()
# Operations history is persisted in a transport-safe form (StringName ids become
# String values), so compare as the Runtime's own restore crosslink does.
var runtime_a: Variant = straight[&"runtime"]
_expect(runtime_a.call("_strict_variants_equal", runtime_a.call("_operations_snapshot_comparison_value", straight_days),
runtime_a.call("_operations_snapshot_comparison_value", file_days)), "the daily closes, fund reports and arrears are the same through save files")
_free_fixture(straight)
_free_fixture(resumed)
_free_fixture(through_files)
func _test_old_save_migration() -> void:
var fixture := _new_fixture(5024)
var runtime: Variant = fixture[&"runtime"]
_advance_to(fixture, 420)
var current: Dictionary = runtime.create_snapshot()
var old := current.duplicate(true)
var economy: Dictionary = old[&"systems"][&"runtime"][&"economy"]
var kept: Array = []
for account: Dictionary in economy[&"accounts"]:
if String(account[&"account_id"]) not in ["BACK_CASH", "RANK_TAX_EXPENSE", "RANK_TAX_PAYABLE", "LOAN_INTEREST_EXPENSE", "LOAN_INTEREST_PAYABLE"]:
kept.append(account)
economy[&"accounts"] = kept
_expect(runtime.restore_snapshot(old), "A12: a save from before the split loads")
_expect(runtime.economy_service.get_balance(FRONT) == 5000 and runtime.economy_service.get_balance(BACK) == 0, "A12: every Gold is front funds, hidden funds start at 0")
_expect(var_to_bytes(runtime.create_snapshot()) == var_to_bytes(current), "A12: the migrated save equals a current save of the same game")
var partial := old.duplicate(true)
var partial_accounts: Array = partial[&"systems"][&"runtime"][&"economy"][&"accounts"]
partial_accounts.append({&"account_id": "RANK_TAX_PAYABLE", &"allow_negative": true, &"system": false})
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.restore_snapshot(partial) and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "a partial set of fund accounts is rejected and changes nothing")
_free_fixture(fixture)
func _test_repayment_top_up() -> void:
var fixture := _new_fixture(5025)
var runtime: Variant = fixture[&"runtime"]
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow"), "fixture principal")
_expect(_sale(fixture, BACK_SALES, BACK, 300, "back") and _move(fixture, FRONT, &"OPERATING_EXPENSE", 5980, &"OPERATING_EXPENSE", "cost"), "front 20, hidden 300")
var receipts: Array = []
runtime.economy_service.transaction_posted.connect(func(id: StringName, _entries: Array) -> void: receipts.append(id))
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.repay_loan_principal(&"p05.fixture", 400, &"p05.repay.too_much").is_success() and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "front + hidden short: repayment rejected, nothing changes")
var paid: CommandResult = runtime.repay_loan_principal(&"p05.fixture", 200, &"p05.repay")
_expect(paid.is_success() and int(paid.details.get(&"outstanding_principal", -1)) == 800, "repayment uses hidden funds for the rest")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 120, "front 20 first, then 180 from hidden funds")
_expect(receipts.size() == 1, "payment and top-up are one transaction")
var entries: Array = runtime.economy_service.get_transaction_entries(StringName(paid.details[&"transaction_id"]))
_expect(entries.size() == 2 and entries[0].category == FinancePolicy.TOP_UP_CATEGORY and entries[1].category == &"LOAN_PRINCIPAL_REPAID", "top-up then principal")
_expect(fixture[&"state"].get_resource(&"gold") == 0, "Gold mirrors front funds after repayment")
_free_fixture(fixture)
func _test_purchase_top_up() -> void:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect(bool(runtime.initialize(clock, state, 5026).get(&"valid", false)), "purchase fixture Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [[&"warehouse", Vector3i(-8, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
_expect(runtime.submit_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}).is_success(), "build %s" % row[0])
clock.advance_minutes(180)
var warehouse := &""
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse":
warehouse = facility.instance_id
_expect(not warehouse.is_empty(), "warehouse completes")
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime}
for account: StringName in [BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
_expect(runtime.economy_service.initialize_account(PAYER, 1000, clock.absolute_minute, &"p05.purchase.payer").is_success(), "fixture payer")
var front: int = runtime.economy_service.get_balance(FRONT)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", front - 2, &"OPERATING_EXPENSE", "purchase.drain") and _sale(fixture, BACK_SALES, BACK, 100, "purchase.back"), "front 2, hidden 100")
var preview: Dictionary = runtime.preview_supply_purchase(warehouse, SupplyCatalog.RAW_FOOD, 5)
_expect(bool(preview.get(&"ok", false)) and int(preview[&"total_cost"]) == 15 and int(preview[&"cash_after"]) == 87, "A09: the quote counts front + hidden funds")
var paid: CommandResult = runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 5})
_expect(paid.is_success(), "the purchase uses hidden funds for what front funds cannot pay")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 87, "front 2 first, then 13 from hidden funds")
var purchase_entries: Array = runtime.economy_service.get_transaction_entries(StringName(paid.details.get(&"transaction_id", &"")))
_expect(purchase_entries.size() == 1 and purchase_entries[0].credit_account == FRONT and purchase_entries[0].amount == 15, "the purchase receipt keeps its single front-fund entry")
var top_ups := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY:
top_ups += entry.amount
_expect(top_ups == 13, "one 13 Gold top-up from hidden funds")
_expect(state.get_resource(&"gold") == 0, "Gold mirrors front funds after the purchase")
# A refused command is still recorded as processed; the funds and stock must not move.
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
var refused: CommandResult = runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
_expect(not refused.is_success() and StringName(refused.details.get(&"reason", &"")) == &"INSUFFICIENT_FUNDS", "front + hidden short: purchase refused for funds")
_expect(var_to_bytes(economy_before) == var_to_bytes(runtime.economy_service.create_snapshot()) \
and var_to_bytes(inventory_before) == var_to_bytes(runtime.inventory_service.create_snapshot()), "the refused purchase moves no Gold and no stock")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "purchase with a top-up restores exactly")
runtime.free()
clock.free()
state.free()
func _test_upkeep_top_up() -> void:
var fixture := _new_fixture(5027)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
_expect(runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}).is_success(), "build a warehouse")
clock.advance_minutes(180)
_advance_to(fixture, 600)
var front: int = runtime.economy_service.get_balance(FRONT)
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", front, &"OPERATING_EXPENSE", "upkeep.drain") and _sale(fixture, BACK_SALES, BACK, 1000, "upkeep.back"), "front 0, hidden 1000")
_advance_to(fixture, 1440)
var summary: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var maintenance := int(summary.get(&"category_totals", {}).get(&"maintenance", -1))
var funds: Dictionary = summary.get(&"settlement", {}).get(&"funds", {})
_expect(maintenance > 0, "the warehouse owes upkeep for day 1")
_expect(int(funds.get(&"back_top_up", -1)) == maintenance + int(funds.get(&"rank_tax", 0)), "A09: hidden funds cover the upkeep (and tax) front funds cannot pay")
_expect(runtime.upkeep_settlement.get_arrears_total() == 0 and int(summary[&"settlement"][&"arrears_total"]) == 0, "no upkeep arrears")
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == 1000 - int(funds[&"back_top_up"]), "front 0, hidden paid the rest")
_free_fixture(fixture)
func _command(runtime: Variant, type: StringName, payload: Dictionary) -> bool:
var result: CommandResult = runtime.submit_command(type, payload)
if not result.is_success():
push_error("%s: %s %s" % [type, result.message, result.details.get(&"reason", &"")])
return result.is_success()
func _facility_of(runtime: Variant, definition_id: StringName) -> StringName:
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == definition_id:
return facility.instance_id
return &""
func _day_funds(runtime: Variant, day: int) -> Dictionary:
return runtime.operations_coordinator.get_day_summary(day).get(&"settlement", {}).get(&"funds", {})
func _closed_ledger_text(runtime: Variant, clock: Variant) -> String:
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, clock)
panel.select_latest_close()
var text: String = panel._closed_label.text
panel.queue_free()
return text
# F1/F5 and F2 (wages) on the production paths. A front facility (massage_room,
# staffed by an ordinary hire on a DAY assignment) and the registered hidden-service
# facility (adult_private_room, underground, its service_attendant on a NIGHT
# assignment) are built and open; the arriving guests choose and complete the
# services themselves and the 00:00 closes settle the day.
func _test_actual_service_funds() -> void:
var fixture := _new_fixture(5031)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
# Rank 2 opens the underground, by the original room-zone suite's own route.
_expect(_command(runtime, &"change_resources", {&"deltas": {&"gold": 5000, &"materials": 100}})
and _command(runtime, &"update_progression_metrics", {&"metrics": {&"reputation": 25}})
and _command(runtime, &"set_world_tree_rank", {&"rank": 2}), "rank 2 through the public commands")
for x: int in range(0, 4):
_expect(_command(runtime, &"start_excavation", {&"cell": Vector3i(x, -1, 0)}), "excavate (%d, -1)" % x)
clock.advance_minutes(60)
# Guests go down by a guest corridor and stairs, staff by a service corridor and stairs.
_expect(_command(runtime, &"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(0, -1, 0)}), "underground guest corridor")
for x: int in range(1, 4):
_expect(_command(runtime, &"build_facility", {&"definition_id": &"service_corridor", &"origin": Vector3i(x, -1, 1)}), "underground service corridor (%d, -1)" % x)
clock.advance_minutes(30)
_expect(_command(runtime, &"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(0, 0, 0)})
and _command(runtime, &"build_facility", {&"definition_id": &"service_stairs", &"origin": Vector3i(3, 0, 1)}), "stairs down for guests and staff")
clock.advance_minutes(30)
_expect(not runtime.path_service.find_path(Vector3i(-2, 0, 0), Vector3i(0, -1, 0)).is_empty(), "guests can walk down to the room")
_expect(_command(runtime, &"build_facility", {&"definition_id": &"adult_private_room", &"origin": Vector3i(1, -1, 0)})
and _command(runtime, &"build_facility", {&"definition_id": &"massage_room", &"origin": Vector3i(-6, 0, 0)}), "build the hidden-service and the front-service facility")
clock.advance_minutes(240)
var back_room := _facility_of(runtime, &"adult_private_room")
var front_room := _facility_of(runtime, &"massage_room")
_expect(not back_room.is_empty() and not front_room.is_empty(), "both facilities complete")
_expect(_command(runtime, &"hire_staff_candidate", {&"candidate_id": &"staff_petra", &"current_cell": Vector3i(-5, 0, 0)})
and _command(runtime, &"set_staff_work_assignment", {&"staff_id": &"staff_petra", &"facility_id": front_room, &"template_id": &"DAY"}), "an ordinary hire works the massage room by day")
# No hire candidate holds the room's service_attendant role (REPAIR_02.md), so it
# is registered through the Runtime's register_staff command.
_expect(_command(runtime, &"register_staff", {&"staff_id": &"p05_attendant", &"role_id": &"service_attendant", &"options": {
&"current_cell": Vector3i(3, 0, 1), &"state": StaffRuntime.State.OFF_DUTY,
&"permitted_task_tags": [&"service", &"work", &"adult_service"], &"equipment_tags": [&"uniform"],
&"role_tags": [&"service_attendant"], &"skills": {&"service": 75}}})
and _command(runtime, &"set_staff_work_assignment", {&"staff_id": &"p05_attendant", &"facility_id": back_room, &"template_id": &"NIGHT"}), "the room's attendant works by night")
clock.advance_minutes(721 - int(clock.absolute_minute))
_expect(runtime.facility_service.get_instance(front_room).state == FacilityInstance.State.OPEN, "the massage room opens with its day staff")
clock.advance_minutes(1141 - int(clock.absolute_minute))
_expect(runtime.facility_service.get_instance(back_room).state == FacilityInstance.State.OPEN, "the room opens with its night staff")
clock.advance_minutes(1440 - int(clock.absolute_minute))
# Day 1: every sale of the massage room went to front funds, of the room to hidden funds.
var front_sales := 0
var back_sales := 0
var misrouted := 0
var back_payers := {}
for entry: LedgerEntry in economy.get_entries():
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == back_room and String(entry.credit_account).begins_with("GUEST_WALLET:guest-auto-"):
back_payers[entry.credit_account] = true
if entry.category != &"SERVICE_CASH_SWEEP" or entry.absolute_minute >= 1440:
continue
if entry.source_entity_id == front_room and entry.debit_account == FRONT:
front_sales += entry.amount
elif entry.source_entity_id == back_room and entry.debit_account == BACK:
back_sales += entry.amount
else:
misrouted += 1
_expect(front_sales > 0 and back_sales > 0 and misrouted == 0, "front service sales sweep to PLAYER_CASH, hidden ones to BACK_CASH (front %d, hidden %d)" % [front_sales, back_sales])
_expect(not back_payers.is_empty(), "arriving guests went down to the room and paid for its service themselves (%d guests)" % back_payers.size())
var day1: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var funds1 := _day_funds(runtime, 1)
_expect(int(funds1.get(&"revenue_front", -1)) == front_sales and int(funds1.get(&"revenue_back", -1)) == back_sales, "the 00:00 close reports the day's sales by fund")
_expect(int(day1[&"category_totals"][&"revenue"]) == front_sales + back_sales, "sales are front + hidden")
_expect(int(funds1.get(&"rank_tax", -1)) == FinancePolicy.rank_tax(front_sales + back_sales) and int(funds1.get(&"rank_tax", -1)) == (front_sales + back_sales) / 10
and int(funds1.get(&"rank_tax_paid", -1)) == int(funds1[&"rank_tax"]), "A10: the close taxes 10% of both sales, paid")
_expect(int(funds1.get(&"back_top_up", -1)) == 0 and economy.get_balance(BACK) == back_sales and _fund_delta(runtime, BACK, 0, 1440) == back_sales, "hidden funds hold exactly the day's hidden sales")
# Day 2: the front fund is lowered to 5 by a ledger fixture before the close, so the
# close must pay the real wages, upkeep and tax with hidden funds.
clock.advance_minutes(1440 + 1430 - int(clock.absolute_minute))
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT) - 5, &"OPERATING_EXPENSE", "service.d2.drain"), "front funds lowered to 5")
var back_before: int = economy.get_balance(BACK)
clock.advance_minutes(2880 - int(clock.absolute_minute))
var day2: Dictionary = runtime.operations_coordinator.get_day_summary(2)
var totals2: Dictionary = day2.get(&"category_totals", {})
var funds2 := _day_funds(runtime, 2)
var due := int(totals2.get(&"wages", 0)) + int(totals2.get(&"maintenance", 0)) + int(totals2.get(&"taxes_and_interest", 0))
_expect(int(totals2.get(&"wages", 0)) > 0 and int(funds2.get(&"revenue_back", 0)) > 0, "day 2 has real wages and hidden sales")
_expect(int(funds2.get(&"back_top_up", -1)) == due - 5, "A09: hidden funds cover the wages, upkeep and tax the 5 front funds cannot pay")
_expect(int(day2[&"settlement"].get(&"unpaid_after_total", -1)) == 0 and int(day2[&"settlement"][&"arrears_total"]) == 0, "the wages are paid in full, nothing is in arrears")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == back_before - (due - 5), "front 0, hidden funds paid the rest")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the two service days restore exactly")
_free_fixture(fixture)
func _has_key(dictionary: Dictionary, key: String) -> bool:
return dictionary.has(StringName(key)) or dictionary.has(key)
func _set_key(dictionary: Dictionary, key: String, value: Variant) -> void:
if dictionary.has(key):
dictionary[key] = value
else:
dictionary[StringName(key)] = value
func _erase_key(dictionary: Dictionary, key: String) -> void:
dictionary.erase(StringName(key))
dictionary.erase(key)
# The saved fund report of a day inside a Runtime snapshot; changing it changes the snapshot.
func _saved_funds(snapshot: Dictionary, day: int) -> Dictionary:
for summary: Dictionary in snapshot[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"day_summaries"]:
if int(summary.get(&"day", summary.get("day", -1))) == day:
var settlement: Dictionary = summary.get(&"settlement", summary.get("settlement", {}))
return settlement.get(&"funds", settlement.get("funds", {}))
return {}
# F2 (automatic purchase), F4 (interest after a repayment) and the day's top-up
# total: the warehouse's automatic order and the repayment command run on their
# production paths with too little front funds, then the 00:00 close. A report
# saved before the definition mark is then loaded by its own definition.
func _test_operation_top_ups_and_day_total() -> void:
var fixture := _new_fixture(5032)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
_expect(_command(runtime, &"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(-8, 0, 1)}), "build a warehouse")
clock.advance_minutes(180)
var warehouse := _facility_of(runtime, &"warehouse")
_expect(not warehouse.is_empty(), "the warehouse completes")
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "ops.borrow"), "fixture principal 1000")
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT) - 2, &"OPERATING_EXPENSE", "ops.drain")
and _sale(fixture, BACK_SALES, BACK, 1000, "ops.back"), "front 2, hidden 1000")
_expect(_command(runtime, &"set_supply_auto_purchase", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"enabled": true, &"target_stock": 10}), "an automatic order for 10 raw food")
clock.advance_minutes(1)
var plan: Dictionary = runtime.auto_purchase_plans.get_plan(warehouse, SupplyCatalog.RAW_FOOD)
var receipt := SupplyAutoPurchasePlans.receipt_from_ledger(economy, warehouse, SupplyCatalog.RAW_FOOD, clock.absolute_minute, StringName(plan.get(&"last_transaction_id", &"")))
_expect(plan.get(&"last_result") == &"PURCHASED" and receipt == {&"item_id": String(SupplyCatalog.RAW_FOOD), &"quantity": 10, &"unit_price": 3, &"total_cost": 30}, "the automatic order buys 10 for 30 and its receipt verifies")
var order_key := SupplyAutoPurchasePlans.order_key(warehouse, SupplyCatalog.RAW_FOOD, clock.absolute_minute)
var order_top_up: Array = economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(StringName("finance.back_top_up.purchase." + String(order_key))))
_expect(order_top_up.size() == 1 and order_top_up[0].category == FinancePolicy.TOP_UP_CATEGORY and order_top_up[0].amount == 28, "A09: hidden funds cover the 28 the automatic order's front funds cannot pay")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 972, "front 0, hidden 972 after the automatic order")
var repaid: CommandResult = runtime.submit_command(&"repay_loan_principal", {&"loan_id": &"p05.fixture", &"amount": 200}, &"player", &"p05.fixture", 50, -1, &"p05.ops.repay")
_expect(repaid.is_success() and int(repaid.details.get(&"outstanding_principal", -1)) == 800, "the repayment command pays 200 of the 1000")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 772, "hidden funds paid the repayment")
clock.advance_minutes(1440 - int(clock.absolute_minute))
var day1: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var funds := _day_funds(runtime, 1)
_expect(int(funds.get(&"loan_interest", -1)) == 8 and int(funds.get(&"loan_interest_paid", -1)) == 8, "A11: the close charges 1% of the 800 left after the repayment, 8, paid")
_expect(int(funds.get(&"rank_tax", -1)) == 100 and int(funds.get(&"rank_tax_paid", -1)) == 100, "the hidden sale's tax is paid")
var close_top_up := 0
for entry: LedgerEntry in economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(&"finance.back_top_up.day.00000001")):
close_top_up += entry.amount
var every_top_up := 0
for entry: LedgerEntry in economy.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY:
every_top_up += entry.amount
_expect(close_top_up == 8 + 100 + int(day1[&"category_totals"][&"maintenance"]), "the close tops up the interest, tax and upkeep")
var total := 28 + 200 + close_top_up
_expect(int(funds.get(&"back_top_up", -1)) == total and every_top_up == total, "funds.back_top_up is the day's every top-up: the order, the repayment and the close")
_expect(int(funds.get(&"fund_report_version", -1)) == 2, "the report carries its definition version")
_expect(economy.get_balance(BACK) == 1000 - total and economy.get_balance(FRONT) == 0, "hidden funds fell by exactly the day's top-ups")
_expect(_closed_ledger_text(runtime, clock).contains("裏金から表金へ補填 %dG" % total), "the ledger's close shows the day's every top-up")
# Construction is outside A09: with no front funds it is refused and the hidden fund is not used.
var refused: CommandResult = runtime.submit_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)})
var top_ups_after := 0
for entry: LedgerEntry in economy.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY:
top_ups_after += entry.amount
_expect(not refused.is_success() and economy.get_balance(BACK) == 1000 - total and top_ups_after == total,
"A09 does not cover construction: refused with front 0, hidden funds untouched (%s)" % refused.details.get(&"reason", refused.message))
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the day with three top-ups restores exactly")
# A report saved before the mark (back_top_up = the close's own top-up only) loads by that definition.
var old := saved.duplicate(true)
var old_funds := _saved_funds(old, 1)
_expect(_has_key(old_funds, "fund_report_version"), "the saved report holds the mark")
_erase_key(old_funds, "fund_report_version")
_set_key(old_funds, "back_top_up", close_top_up)
_expect(runtime.restore_snapshot(old), "an unmarked report with the close-only top-up loads")
var loaded := _day_funds(runtime, 1)
_expect(int(loaded.get(&"back_top_up", -1)) == close_top_up and not _has_key(loaded, "fund_report_version"), "the loaded old report stays as it was saved")
_expect(_closed_ledger_text(runtime, clock).contains("裏金から表金へ補填(締めの時の分のみ) %dG" % close_top_up), "the ledger labels the old report's close-only top-up")
# No looser check: each definition accepts only its own value.
for case: Array in [[null, total, "an unmarked report with the day total"], [2, close_top_up, "a marked report with the close-only top-up"], [3, total, "an unknown mark"]]:
var bad := saved.duplicate(true)
var bad_funds := _saved_funds(bad, 1)
if case[0] == null:
_erase_key(bad_funds, "fund_report_version")
else:
_set_key(bad_funds, "fund_report_version", case[0])
_set_key(bad_funds, "back_top_up", case[1])
var before: Dictionary = runtime.create_snapshot()
_expect(not runtime.restore_snapshot(bad) and var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "%s is rejected and changes nothing" % case[2])
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the current save loads again")
_free_fixture(fixture)
# F2 (legacy calendar): in a save from before the 00:00 close the upkeep is paid at
# calendar midnight and the financial day closes at 06:00 (as in
# tests/mvp/test_upkeep_legacy_midnight.gd). With no front funds the midnight upkeep
# is topped up from hidden funds, and that 06:00 day's report counts it.
func _test_legacy_midnight_top_up() -> void:
var fixture := _new_fixture(5033)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
var definition: FacilityDefinition = runtime.definition_registry.get_facility(&"guest_corridor")
var old_rate := definition.maintenance_gold
definition.maintenance_gold = 1440
_expect(_command(runtime, &"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)}), "build a corridor with upkeep")
runtime.operations_coordinator._legacy_calendar_last_day = 1
clock.advance_minutes(1079)
var legacy: Dictionary = runtime.create_snapshot()
var data: Dictionary = legacy[&"systems"][&"runtime"]
data[&"runtime_schema_version"] = 9
preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(data)
data[&"operations"][&"coordinator"][&"schema_version"] = 3
data[&"operations"][&"coordinator"].erase(&"legacy_calendar_last_day")
data[&"operations"][&"coordinator"].erase(&"legacy_history_last_day")
data[&"operations"][&"coordinator"].erase(&"legacy_principal_profit_last_day")
_expect(runtime.restore_snapshot(legacy), "the legacy-calendar save loads at 23:59")
# The load replaces the Runtime's ledger service.
economy = runtime.economy_service
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT), &"OPERATING_EXPENSE", "legacy.drain")
and _sale(fixture, BACK_SALES, BACK, 500, "legacy.back"), "front 0, hidden 500")
clock.advance_minutes(1)
var midnight: Array = economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(&"finance.back_top_up.midnight.1440"))
_expect(midnight.size() == 1 and midnight[0].category == FinancePolicy.TOP_UP_CATEGORY and midnight[0].amount == 1, "A09: hidden funds cover the 00:00 upkeep front funds cannot pay")
_expect(economy.get_balance(FacilityUpkeepSettlement.EXPENSE) == 1 and runtime.upkeep_settlement.get_arrears_total() == 0
and economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 499, "the midnight upkeep is paid, nothing in arrears")
clock.advance_minutes(360)
var day1: Dictionary = runtime.operations_coordinator.get_day_summary(1)
var funds := _day_funds(runtime, 1)
_expect(int(day1.get(&"closed_at", -1)) == 1800 and int(funds.get(&"rank_tax", -1)) == 50 and int(funds.get(&"rank_tax_paid", -1)) == 50, "the 06:00 close taxes the hidden sale, paid")
_expect(int(funds.get(&"back_top_up", -1)) == 51 and economy.get_balance(BACK) == 449, "the 06:00 day's report counts the midnight top-up and the close's")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the legacy day restores exactly")
definition.maintenance_gold = old_rate
_free_fixture(fixture)
# F3: the day's tax that front and hidden funds cannot pay stays payable, and the
# next close pays that older tax before the new one.
func _test_tax_arrears_next_day() -> void:
var fixture := _new_fixture(5034)
var runtime: Variant = fixture[&"runtime"]
var economy: Variant = runtime.economy_service
_advance_to(fixture, 420)
_expect(_sale(fixture, FRONT_SALES, FRONT, 200, "tax.d1") and _move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT), &"OPERATING_EXPENSE", "tax.d1.drain"), "day 1 sale 200, then no funds")
_advance_to(fixture, 1440)
var funds1 := _day_funds(runtime, 1)
_expect(int(funds1.get(&"rank_tax", -1)) == 20 and int(funds1.get(&"rank_tax_paid", -1)) == 0 and int(funds1.get(&"rank_tax_arrears", -1)) == 20, "A09: the unpaid tax carries over")
_expect(int(runtime.operations_coordinator.get_day_summary(1)[&"settlement"][&"arrears_total"]) == 20 and int(runtime.get_operations_report().get(&"arrears_total", -1)) == 20, "the arrears include the tax")
# F7 with arrears: the ledger shows the unpaid tax in the close and in today's cash section.
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, fixture[&"clock"])
panel.select_latest_close()
_expect(panel._closed_label.text.contains("未払い 税 20G・利息 0G") and panel._cash_label.text.contains("(未払い 20G)"), "the ledger shows the unpaid tax")
panel.queue_free()
# The closed day's arrears_total includes the tax arrears, so the Operations gate holds
# construction and investment until they are paid.
var blocked: CommandResult = runtime.submit_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)})
_expect(not blocked.is_success() and StringName(blocked.details.get(&"reason", &"")) == &"ARREARS_REMAIN" and int(blocked.details.get(&"arrears_total", -1)) == 20,
"tax arrears stop construction (ARREARS_REMAIN, arrears 20)")
_advance_to(fixture, 1440 + 600)
_expect(_sale(fixture, FRONT_SALES, FRONT, 100, "tax.d2"), "day 2 sale 100")
_advance_to(fixture, 2880)
var funds2 := _day_funds(runtime, 2)
_expect(int(funds2.get(&"rank_tax", -1)) == 10 and int(funds2.get(&"rank_tax_paid", -1)) == 30 and int(funds2.get(&"rank_tax_arrears", -1)) == 0, "day 2 pays the older 20 and the new 10")
_expect(economy.get_balance(FRONT) == 70 and economy.get_balance(FinancePolicy.RANK_TAX_PAYABLE) == 0, "front 70, no tax payable left")
var built: CommandResult = runtime.submit_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)})
_expect(built.is_success() and economy.get_balance(FRONT) == 50, "once the tax is paid, construction is allowed again (20 from front funds)")
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the arrears days restore exactly")
_free_fixture(fixture)
# F2 through the ordinary ledger: the panel's repayment controls cap by front + hidden
# funds (A09), and a specified or full repayment that front funds cannot pay is one
# transaction: the front fund pays first and the hidden fund covers the rest.
func _test_ledger_repayment_uses_hidden_funds() -> void:
var fixture := _new_fixture(5035)
var runtime: Variant = fixture[&"runtime"]
var clock: Variant = fixture[&"clock"]
var economy: Variant = runtime.economy_service
_advance_to(fixture, 420)
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "panel.borrow"), "fixture principal 1000")
_expect(_move(fixture, FRONT, &"OPERATING_EXPENSE", economy.get_balance(FRONT) - 20, &"OPERATING_EXPENSE", "panel.drain")
and _sale(fixture, BACK_SALES, BACK, 300, "panel.back"), "front 20, hidden 300")
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, clock)
panel.loan_amount.text = "200"
panel.loan_amount.text_changed.emit("200")
var hint: String = panel.get_node("LoanRepaymentHint").text
_expect(not panel.repay_button.disabled and hint.contains("表金 20G・裏金 300G(計 320G)") and hint.contains("裏金から補填"), "a 200 repayment over front 20 is offered with the hidden fund")
var key := StringName("ui.loan.repay.%d" % int(runtime.get_loan_principal_report()[&"next_command_id"]))
panel.repay_button.pressed.emit()
var entries: Array = economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(key))
_expect(entries.size() == 2 and entries[0].category == FinancePolicy.TOP_UP_CATEGORY and entries[0].amount == 180 and entries[1].category == &"LOAN_PRINCIPAL_REPAID" and entries[1].amount == 200,
"one transaction: 180 hidden top-up, then the 200 principal")
_expect(economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 120 and -economy.get_balance(LOAN) == 800 and panel.get_node("LoanRepaymentResult").text.contains("200Gを返済"),
"front 20 first, hidden 180 for the rest; 800 left")
_expect(panel.repay_all_button.disabled and panel.get_node("LoanRepaymentHint").text.contains("完済には表金と裏金の合計が不足しています"), "full repayment of 800 waits: front + hidden is 120")
panel.loan_amount.text = "500"
panel.loan_amount.text_changed.emit("500")
_expect(panel.repay_button.disabled, "500 over front + hidden 120 is not offered")
var before: Dictionary = runtime.create_snapshot()
panel.submit_repayment(true)
panel.submit_repayment(false)
_expect(var_to_bytes(before) == var_to_bytes(runtime.create_snapshot()), "pressed anyway, the unaffordable repayments change nothing")
_expect(_sale(fixture, BACK_SALES, BACK, 700, "panel.back2"), "hidden funds reach 820")
panel.refresh()
_expect(not panel.repay_all_button.disabled, "full repayment of 800 is offered with front 0 and hidden 820")
var full_key := StringName("ui.loan.repay.%d" % int(runtime.get_loan_principal_report()[&"next_command_id"]))
panel.repay_all_button.pressed.emit()
var full: Array = economy.get_transaction_entries(economy.get_transaction_id_for_idempotency_key(full_key))
_expect(full.size() == 2 and full[0].category == FinancePolicy.TOP_UP_CATEGORY and full[0].amount == 800 and full[1].amount == 800,
"the full repayment is one transaction, all from hidden funds")
_expect(economy.get_balance(LOAN) == 0 and economy.get_balance(FRONT) == 0 and economy.get_balance(BACK) == 20 and fixture[&"state"].get_resource(&"gold") == 0, "the loan is repaid; front 0, hidden 20, Gold mirrors front funds")
panel.queue_free()
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and var_to_bytes(saved) == var_to_bytes(runtime.create_snapshot()), "the panel repayments restore exactly")
_free_fixture(fixture)
# F7: the ordinary ledger panel reads both funds, the rank tax, the interest, the
# top-ups and their arrears from the same ledger and daily closes.
func _check_ledger_panel(runtime: Variant, clock: Variant) -> void:
var panel := OperatingLedgerPanel.new()
root.add_child(panel)
panel.setup(runtime, clock)
var report: Dictionary = panel.get_report()
_expect(int(report.get(&"cash", -1)) == runtime.economy_service.get_balance(FRONT)
and int(report.get(&"back_cash", -1)) == runtime.economy_service.get_balance(BACK), "the panel reports both funds")
_expect(int(report.get(&"finance_arrears", -1)) == 0, "the panel reports no tax or interest arrears after day 4")
var cash_text: String = panel._cash_label.text
_expect(cash_text.contains("裏金 %dG" % runtime.economy_service.get_balance(BACK)) and cash_text.contains("本日の税") and cash_text.contains("利息"), "the cash section shows the hidden fund, tax and interest")
panel.select_latest_close()
var close_text: String = panel._closed_label.text
_expect(close_text.contains("うち表の売上 100G / 裏の売上 0G") and close_text.contains("世界樹ランク税 10G(支払 10G)") \
and close_text.contains("利息 10G(支払 20G)") and close_text.contains("未払い 税 0G・利息 0G"), "the day 4 close shows sales by fund, tax, interest and arrears")
panel.queue_free()
# F7 through ordinary operation: after a real 00:00 close on the game's own
# Runtime and clock, Main's 帳簿 navigation button opens the ledger.
func _test_ledger_opened_from_main() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 5028)
clock.set_paused(true)
root.size = Vector2i(1440, 900)
var main: Control = load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
_expect(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "game Runtime fixture payer funded")
clock.advance_minutes(60)
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime}
_expect(_move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "ui.borrow") and _sale(fixture, FRONT_SALES, FRONT, 300, "ui.front")
and _sale(fixture, BACK_SALES, BACK, 500, "ui.back"), "day 1 loan and sales on the game Runtime")
clock.advance_minutes(1440 - int(clock.absolute_minute))
_expect(not runtime.operations_coordinator.get_day_summary(1).is_empty(), "the normal clock closes day 1 at 00:00")
var ledger_button: Button = (main.get("navigation_buttons") as Dictionary)[&"ledger"]
ledger_button.pressed.emit()
var panel: OperatingLedgerPanel = main.get("operating_ledger_panel")
_expect(panel.visible and ledger_button.button_pressed, "the 帳簿 button opens the ledger")
var report: Dictionary = panel.get_report()
_expect(int(report.get(&"cash", -1)) == 6210 and int(report.get(&"back_cash", -1)) == 500, "the opened ledger reports front 6210 and hidden 500")
# The day 1 tax and interest are paid at the 00:00 that opens day 2.
var cash_text: String = panel._cash_label.text
_expect(cash_text.contains("裏金 500G(本日の入金 +0G / 表金へ補填 −0G)") and cash_text.contains("本日の税 80G / 利息 10G(未払い 0G)"), "the opened ledger shows the hidden fund, today's tax and interest")
var close_text: String = panel._closed_label.text
_expect(close_text.contains("うち表の売上 300G / 裏の売上 500G") and close_text.contains("世界樹ランク税 80G(支払 80G)/ 利息 10G(支払 10G)")
and close_text.contains("裏金から表金へ補填 0G / 未払い 税 0G・利息 0G"), "the opened ledger shows the day 1 close by fund with tax and interest")
var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
_expect(bool(cleanup.get(&"passed", false)), "ordinary Main release with audio cleanup")
@@ -0,0 +1 @@
uid://2v2xxx0grnx8
+161
View File
@@ -0,0 +1,161 @@
extends RefCounted
## P05-02 F6: the fixture and day script shared by the restart writer
## (tests/economy/test_p05_funds_restart_1_write.gd) and reader
## (tests/economy/test_p05_funds_restart_2_read.gd), which run as separate engine
## processes. Same ledger fixture and accelerated boundary traversal as
## tests/economy/test_p05_funds_runtime.gd.
const HANDOFF_PATH := "res://artifacts/p05-02/restart-handoff.json"
const HANDOFF_SCHEMA := "p05-02-restart-handoff-v1"
const WRITER_SOURCE := "res://tests/economy/test_p05_funds_restart_1_write.gd"
const WRITER_SAVE := "user://p05_restart_write/minute_%d"
const READER_SAVE := "user://p05_restart_read/minute_%d"
const READER_TAMPERED_SAVE := "user://p05_restart_read/changed"
const SEED := 5025
# Mid-day 2, just before the day 2 00:00 close (which needs a hidden-fund top-up)
# and just after it.
const SAVE_MINUTES: Array[int] = [2400, 2850, 2880]
const LAST_MINUTE := 4320
const FRONT := &"PLAYER_CASH"
const BACK := &"BACK_CASH"
const LOAN := &"LOAN_LIABILITY:p05.fixture"
const FRONT_SALES := &"P05_FIXTURE_FRONT_SALES"
const BACK_SALES := &"P05_FIXTURE_BACK_SALES"
const PAYER := &"P05_FIXTURE_PAYER"
var _expect: Callable
func _init(expect: Callable) -> void:
_expect = expect
func new_fixture() -> Dictionary:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
_expect.call(bool(runtime.initialize(clock, state, SEED).get(&"valid", false)), "fixture Runtime initializes")
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect.call(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
runtime.economy_service.register_account(LOAN, true)
_expect.call(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p05.fixture.payer").is_success(), "fixture payer funded")
return {&"clock": clock, &"state": state, &"runtime": runtime}
func free_fixture(fixture: Dictionary) -> void:
for key: StringName in [&"runtime", &"clock", &"state"]:
if fixture.get(key) != null:
fixture[key].free()
## Applies the day script's steps after `after_minute` through `through_minute`
## and calls `on_minute` after each step (the writer saves there).
## Day 1: borrow 1000, hidden sale 700 (tax 70, interest 10). Day 2: a cost leaves
## 5 front funds for the 10 interest, so hidden funds top up 5. Day 3: front sale
## 50 (tax 5, interest 10). Final: front 35, hidden 695.
func play(fixture: Dictionary, after_minute: int, through_minute: int, on_minute: Callable) -> void:
var steps: Array = [
[420, func() -> void: _move(fixture, LOAN, FRONT, 1000, &"LOAN_PRINCIPAL_BORROWED", "borrow")],
[450, func() -> void: _sale(fixture, BACK_SALES, BACK, 700, "d1.back")],
[1440, Callable()],
[2040, func() -> void: _move(fixture, FRONT, &"OPERATING_EXPENSE", 5915, &"OPERATING_EXPENSE", "d2.cost")],
[2400, Callable()],
[2850, Callable()],
[2880, Callable()],
[3510, func() -> void: _sale(fixture, FRONT_SALES, FRONT, 50, "d3.front")],
[4320, Callable()],
]
for step: Array in steps:
var minute := int(step[0])
if minute <= after_minute or minute > through_minute:
continue
_advance_to(fixture, minute)
if (step[1] as Callable).is_valid():
(step[1] as Callable).call()
if on_minute.is_valid():
on_minute.call(minute)
func economy_bytes(runtime: Variant) -> PackedByteArray:
return var_to_bytes(runtime.create_snapshot()[&"systems"][&"runtime"][&"economy"])
## Operations history in the transport-safe form the Runtime's own restore
## crosslink compares (StringName ids become String values in saves).
func days_value(runtime: Variant) -> Variant:
return runtime.call("_operations_snapshot_comparison_value", runtime.operations_coordinator.get_day_summaries())
func days_equal(runtime: Variant, expected: Variant) -> bool:
return runtime.call("_strict_variants_equal", expected, days_value(runtime))
func category_count(runtime: Variant, categories: Array) -> int:
var count := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category in categories:
count += 1
return count
static func sha256(bytes: PackedByteArray) -> String:
var context := HashingContext.new()
context.start(HashingContext.HASH_SHA256)
if not bytes.is_empty():
context.update(bytes)
return context.finish().hex_encode()
# Every Operations boundary is on a 30-minute mark; each step ends on one.
func _advance_to(fixture: Dictionary, minute: int) -> void:
var clock: Variant = fixture[&"clock"]
var runtime: Variant = fixture[&"runtime"]
while int(clock.absolute_minute) < minute:
var step := mini(30 - int(clock.absolute_minute) % 30, minute - int(clock.absolute_minute))
var target: int = clock.absolute_minute + step
clock.absolute_minute = target
clock.call("_sync_calendar_fields")
clock.snapshot_version += step
runtime.call("_advance_weather", target)
runtime.call("_advance_facility_upkeep", target)
runtime.call("_advance_operations_time", target)
_align_clocks(runtime, target)
# The fixture owns no actors, cars or rides, so only the empty aggregates' clocks
# follow the minute.
func _align_clocks(runtime: Variant, minute: int) -> void:
for service: Variant in [runtime.inventory_service, runtime.facility_service, runtime.kitchen_production,
runtime.guest_service, runtime.staff_service, runtime.authority_service, runtime.defense_service,
runtime.construction_service, runtime.excavation_service]:
service.current_minute = minute
var transport_snapshot: Dictionary = runtime.elevators.create_snapshot()
if not (transport_snapshot[&"shafts"] as Array).is_empty() or not (transport_snapshot[&"trips"] as Array).is_empty():
_expect.call(false, "accelerated fixture cannot skip live elevator movement")
return
transport_snapshot[&"current_minute"] = minute
if not runtime.elevators.restore_snapshot(transport_snapshot):
_expect.call(false, "empty transport clock aligns through validated snapshot restore")
func _sale(fixture: Dictionary, revenue_account: StringName, fund: StringName, amount: int, key: String) -> void:
var runtime: Variant = fixture[&"runtime"]
var minute: int = fixture[&"clock"].absolute_minute
var entries: Array = [
LedgerEntry.new().configure(&"", minute, revenue_account, PAYER, amount, &"SERVICE_SALE", &"p05.fixture"),
LedgerEntry.new().configure(&"", minute, fund, revenue_account, amount, &"SERVICE_CASH_SWEEP", &"p05.fixture"),
]
_expect.call(runtime.economy_service.post(entries, StringName("p05.sale." + key)).is_success(), "fixture sale " + key)
func _move(fixture: Dictionary, from: StringName, to: StringName, amount: int, category: StringName, key: String) -> void:
var runtime: Variant = fixture[&"runtime"]
_expect.call(runtime.economy_service.transfer(from, to, amount, fixture[&"clock"].absolute_minute, category,
&"p05.fixture", StringName("p05.move." + key)).is_success(), "fixture move " + key)
@@ -0,0 +1 @@
uid://m3lik2gqaho2
+26
View File
@@ -414,6 +414,29 @@
], ],
"old_bounded_teardown": false, "old_bounded_teardown": false,
"new_audio_QA": false "new_audio_QA": false
},
"tests/economy/test_p05_funds_runtime.gd": {
"source_sha256": "4c8f929b0de76198182dfaaef4be4bea7961faa9c31c01889497d6675e0c5def",
"ordinary_cleanup_prepared": true,
"ordinary_control_labels": [
"single"
],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/economy/test_p05_funds_restart_1_write.gd": {
"source_sha256": "3b28e94f1a1f577d74c5322d532b32854339dd4731532ac0b21b4fbc7ffb12f3",
"ordinary_cleanup_prepared": false,
"ordinary_control_labels": [],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/economy/test_p05_funds_restart_2_read.gd": {
"source_sha256": "1b5bbf0f5c58df39ec28cc867c18a1a14d293fd36459343a118a3e97bc31d33a",
"ordinary_cleanup_prepared": false,
"ordinary_control_labels": [],
"old_bounded_teardown": false,
"new_audio_QA": false
} }
}, },
"outside_routes": { "outside_routes": {
@@ -448,6 +471,9 @@
} }
}, },
"fixed_generated_report_routes": { "fixed_generated_report_routes": {
"tests/economy/test_p05_funds_restart_1_write.gd": [
"artifacts/p05-02/restart-handoff.json"
],
"tests/mvp/test_runtime_upkeep.gd": [ "tests/mvp/test_runtime_upkeep.gd": [
"artifacts/upkeep-2026-09-29/preclose-snapshot.txt", "artifacts/upkeep-2026-09-29/preclose-snapshot.txt",
"artifacts/upkeep-2026-09-29/closed-snapshot.txt" "artifacts/upkeep-2026-09-29/closed-snapshot.txt"
+613
View File
@@ -0,0 +1,613 @@
#!/usr/bin/env python3
"""Root-owned narrow P05-02 (front and hidden funds, rank tax, loan interest) collection using the
unchanged guarded phase protocol of tools/run_p18_relationship_tests.py. After the roster, the F6
restart writer and reader suites run as two more phases (separate engine processes); the writer's
only declared generated report is the save handoff the reader loads."""
import argparse
import hashlib
import json
import os
import re
import stat
from pathlib import Path
import shutil
import sys
import uuid
sys.dont_write_bytecode = True
import run_all_tests as shared
ROSTER = [
'tests/economy/test_p05_funds_runtime.gd',
'tests/mvp/test_original_midnight_settlement.gd',
'tests/mvp/test_runtime_operations_integration.gd',
'tests/s2/test_runtime_operations_cycle.gd',
'tests/s6/test_runtime_save_lifecycle.gd',
'tests/s6/test_runtime_snapshot_serialization.gd',
'tests/supply/test_supply_management_service.gd',
'tests/ui/test_upkeep_closed_ui.gd',
]
# P05-02 F6 (SPEC section 6): the writer saves, the reader in the next phase's engine resumes.
RESTART = [
'tests/economy/test_p05_funds_restart_1_write.gd',
'tests/economy/test_p05_funds_restart_2_read.gd',
]
HANDOFF = 'artifacts/p05-02/restart-handoff.json'
SUITES = ROSTER + RESTART
WRITER_LINE = r'(?m)^P05_RESTART_WRITER pid=([0-9]+) handoff_sha256=([0-9a-f]{64})$'
READER_LINE = r'(?m)^P05_RESTART_READER pid=([0-9]+) writer_pid=([0-9]+) handoff_sha256=([0-9a-f]{64})$'
def collect(args, root, out, refs, markers, contracts, policy, contract_path, contract_sha, parse_paths):
source_bytes = sum(pin['bytes'] for pin in refs.values())
free = shutil.disk_usage(out.parent).free
needed = source_bytes + shared.MIN_SPACE
shared.require(free >= needed, 'fresh source and phase disk admission')
outer = shared.module('p05_owned_outer', shared.SUPPORT / 'owned_outer.py')
shared.require(not outer.native.helper['live_engines'](), 'another Godot owns CPU')
engine = shutil.which('godot')
shared.require(engine and Path(engine).is_file(), 'native launcher exists')
engine = str(Path(engine).resolve())
out.mkdir(parents=True, exist_ok=False)
stage = out / 'stage'
stage.mkdir()
(out / 'starters').mkdir()
(out / 'bootstrap').mkdir()
for name, pin in shared.read(shared.SUPPORT / 'bootstrap-scripts.json').items():
data = pin['text'].encode('utf-8')
shared.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'unchanged external bootstrap bytes')
(out / 'bootstrap' / name).write_bytes(data)
protected = shared.default_player(root)
expected = {name: {'bytes': pin['bytes'], 'sha256': pin['sha256']} for name, pin in refs.items()}
selected = {'root_before': refs, 'map': {name: pin['sha256'] for name, pin in refs.items()}, 'timeout': args.timeout}
for name, pin in refs.items():
target = stage / name
target.parent.mkdir(parents=True, exist_ok=True)
shared.copy_source(Path(pin['path']), target)
shared.require(shared.inventory(stage, stage=True) == expected, 'complete private source copy')
public = (stage / 'project.godot').read_bytes()
shared.write(out / 'parse-paths.json', parse_paths)
run_id = uuid.uuid4().hex
shared.write(out / 'runner-input.json', dict(protocol='guarded-p05-02-v1', run_id=run_id,
owner_runner_pid=os.getpid(), project_root=str(root), stage=str(stage), roster=ROSTER,
restart_phases=RESTART, restart_handoff=HANDOFF,
selected_source_map=selected['map'], marker_contract_sha256=contract_sha,
runner_sha256=shared.sha(Path(__file__)), shared_runner_sha256=shared.sha(shared.HERE / 'run_all_tests.py'),
marker_module_sha256=shared.sha(shared.HERE / 'strict_test_markers.py'),
engine=engine, engine_sha256=shared.sha(engine), default_player_before=protected,
formal_acceptance=False, original_unit_complete=False))
shared.write(out / 'source-start.json', refs)
diagnostics = shared.module('p05_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
owners = shared.module('p05_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
audio = shared.module('p05_audio', shared.SUPPORT / 'audio_evidence_gate.py')
phases, rows, previous = [], [], {}
error = None
try:
for phase, test in [('import', None), ('parse', None)] + [(str(i).zfill(3), name) for i, name in enumerate(SUITES)]:
shared.require(shared.sha(contract_path) == contract_sha, 'marker contract unchanged')
row = shared.execute_phase(engine, stage, out, phase, test, selected, expected, previous,
policy, contracts, parse_paths, public, protected, root, outer, markers, diagnostics, owners, audio)
archive_profiles(out, row)
phases.append(row)
if test:
row.update(sequence=len(rows), godot=engine)
rows.append(row)
(out / 'all-tests.json').write_text(json.dumps(rows, ensure_ascii=False, indent=2) + '\n')
print(('PASS ' if row['passed'] else 'FAIL ') + test, flush=True)
shared.require(row['passed'], 'first failed phase stops without retry: ' + phase)
except BaseException as caught:
error = type(caught).__name__ + ': ' + str(caught)
finally:
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
guard_errors = []
try:
shared.require(shared.source_refs(root) == refs, 'whole source unchanged')
shared.require(shared.sha(contract_path) == contract_sha, 'marker contract unchanged at end')
shared.require(shared.default_player(root) == protected, 'default profile untouched')
shared.require(shared.inventory(stage, stage=True) == dict(expected, **previous), 'whole stage exact')
except BaseException as caught:
guard_errors.append(type(caught).__name__ + ': ' + str(caught))
ended = bool(outer.OWNED) and all(row['engine_ended'] is True for row in cleanup)
passed = error is None and not cleanup_errors and not guard_errors and ended and len(phases) == len(SUITES) + 2 and all(row['passed'] for row in phases)
context = shared.totals(rows, SUITES)
shared.write(out / 'runner-ended.json', dict(context, protocol='guarded-p05-02-v1', run_id=run_id,
owner_runner_pid=os.getpid(), passed=passed, engine_ended=ended,
actual_captured_PIDs=[process.pid for process in outer.OWNED], final_owned_cleanup=cleanup,
cleanup_errors=cleanup_errors, guard_errors=guard_errors, error=error, phases=phases,
all_tests_sha256=shared.sha(out / 'all-tests.json') if (out / 'all-tests.json').exists() else None,
default_player_after=shared.default_player(root), formal_acceptance=False,
original_unit_complete=False, independent_review='pending', native_Windows='pending'))
if passed:
seal(out, refs, engine, contract_sha)
# Root also exercises precisely the verifier that ordinary Runner uses.
verify(out, refs, markers, contracts, policy, contract_sha, parse_paths)
print(json.dumps(dict(context, passed=passed, error=error, cleanup_errors=cleanup_errors, guard_errors=guard_errors)), flush=True)
return 0 if passed else 1
# Receipt file names are always relative; absolute paths in native receipts are
# historical identity strings only and are never opened by the verifier.
def require(ok, reason):
shared.require(ok, reason)
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'),
ensure_ascii=False).encode()).hexdigest()
def content_map(refs):
return {name: {'bytes': pin['bytes'], 'sha256': pin['sha256']}
for name, pin in refs.items()}
def plain_path(path):
path = Path(path)
require('..' not in path.parts, 'parent traversal refused')
absolute = Path(os.path.abspath(path))
for part in [absolute, *absolute.parents]:
require(not part.is_symlink(), 'symlink path refused: ' + str(part))
return absolute
def confined(out, name):
require(type(name) is str and name and '\\' not in name, 'ordinary relative evidence name')
path = Path(name)
require(not path.is_absolute() and '..' not in path.parts
and path.as_posix() == name and name != '.', 'evidence path escape or alias')
result = plain_path(out / path)
require(result.is_relative_to(out), 'evidence confined to output')
return result
def inventory(out):
result = {}
require(out.is_dir(), 'output directory required')
for directory, dirs, files in os.walk(out, followlinks=False):
for name in dirs + files:
p = Path(directory) / name
mode = p.lstat().st_mode
require(stat.S_ISREG(mode) or stat.S_ISDIR(mode), 'nonordinary evidence refused')
if stat.S_ISREG(mode):
require(p.stat().st_nlink == 1, 'hardlinked evidence refused')
result[p.relative_to(out).as_posix()] = {
'bytes': p.stat().st_size, 'sha256': shared.sha(p)}
return result
def unique_object(pairs):
result = {}
for key, value in pairs:
require(key not in result, 'duplicate JSON key')
result[key] = value
return result
def read_json(path):
return json.loads(path.read_text(encoding='utf-8'), object_pairs_hook=unique_object,
parse_constant=lambda token: require(False, 'nonfinite JSON ' + token))
def archive_profiles(out, row):
for label in ('startup', 'runtime'):
value = row.get('whole_' + label + '_profile_after')
if value is None:
continue
dest = out / 'profiles' / row['phase'] / label
dest.mkdir(parents=True, exist_ok=False)
source = plain_path(value['path'])
require(shared.inventory(source) == value['files'], 'whole owned profile before archive')
for name in value['files']:
target = confined(out, str((dest / name).relative_to(out)))
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source / name, target)
require(shared.inventory(dest) == value['files'] == shared.inventory(source),
'whole owned profile archive exact')
def seal(out, refs, engine, contract_sha):
# Last file written: an interrupted collection has no complete receipt.
shutil.copyfile(engine, out / 'engine.bin')
require(shared.sha(out / 'engine.bin') == shared.sha(engine), 'engine archive exact')
source = content_map(refs)
shared.write(out / 'receipt.json', {
'schema': 'guarded-p05-02-v1', 'source_map': source,
'parse_checks': len([name for name in refs if name.endswith('.gd')]),
'source_map_sha256': digest(source),
'project_settings_sha256': source['project.godot']['sha256'],
'marker_contract_sha256': contract_sha,
'engine_sha256': shared.sha(engine),
'files': inventory(out),
})
def cleanup_gate(value):
require(value['owned_child_created'] is True and value['engine_ended'] is True
and value['actual_process_exit_code'] == 0 and value['errors'] == [],
'successful actual owned cleanup')
attempts = value['attempts']
require(1 <= len(attempts) <= 3, 'bounded cleanup attempts')
for index, row in enumerate(attempts):
require(row['attempt'] == index + 1 and row['errors'] == []
and row['wait_timeout_seconds'] == 5 and row['wait_completed'] is True
and row['wait_returned_code'] == 0 and row['poll_after'] == 0,
'complete bounded owned wait')
def outer_cleanup(rows, pids):
require([r['pid'] for r in rows] == pids, 'exact captured cleanup PID order')
for row in rows:
require(row['engine_ended'] is True and row['outer_cleanup_errors'] == []
and row['foreign_process_or_group_signals'] == 0
and row['outer_maximum_helper_invocations'] == 3
and row['helper_maximum_waits'] == 3 and row['per_wait_timeout_seconds'] == 5,
'unchanged outer cleanup contract')
cleanup_gate(row['cleanup'])
def verify(out, refs, markers, contracts, policy, contract_sha, parse_paths):
receipt_path = confined(out, 'receipt.json')
require(receipt_path.is_file(), 'regular final receipt required')
manifest = read_json(receipt_path)
require(manifest['schema'] == 'guarded-p05-02-v1', 'current receipt schema required')
files = manifest['files']
require(type(files) is dict and files, 'complete raw manifest')
for name, pin in files.items():
p = confined(out, name)
require(p.is_file() and set(pin) == {'bytes', 'sha256'}
and type(pin['bytes']) is int and pin['bytes'] >= 0
and re.fullmatch('[0-9a-f]{64}', pin['sha256']) is not None,
'ordinary artifact pin')
actual = inventory(out)
receipt_pin = actual.pop('receipt.json')
require(actual == files, 'complete raw file set and SHA-256 equality')
source = content_map(refs)
require(manifest['source_map'] == source and manifest['source_map_sha256'] == digest(source),
'current whole source content map independent of checkout path')
require(manifest['project_settings_sha256'] == source['project.godot']['sha256']
and manifest['marker_contract_sha256'] == contract_sha, 'current project/marker pins')
def read(name):
require(name in files, 'required raw file missing: ' + name)
return read_json(confined(out, name))
def text(name):
require(name in files, 'required raw transport missing: ' + name)
return confined(out, name).read_text(encoding='utf-8')
start, end = read('runner-input.json'), read('runner-ended.json')
require(start['protocol'] == end['protocol'] == 'guarded-p05-02-v1'
and start['run_id'] == end['run_id']
and re.fullmatch('[0-9a-f]{32}', start['run_id']) is not None,
'same current run')
require(type(start['owner_runner_pid']) is int and start['owner_runner_pid'] > 0
and start['owner_runner_pid'] == end['owner_runner_pid'], 'same collector owner')
require(start['roster'] == ROSTER and start['restart_phases'] == RESTART
and start['restart_handoff'] == HANDOFF and len(set(SUITES)) == len(SUITES),
'exact unique roster and restart tests')
require(start['selected_source_map'] == {n: p['sha256'] for n, p in source.items()}
and content_map(read('source-start.json')) == source, 'whole selected source pins')
for field, name in [('runner_sha256', 'tools/run_p05_funds_tests.py'),
('shared_runner_sha256', 'tools/run_all_tests.py'),
('marker_module_sha256', 'tools/strict_test_markers.py')]:
require(start[field] == source[name]['sha256'], 'current collector/shared/marker pin')
require(start['marker_contract_sha256'] == contract_sha, 'marker contract pin')
# Resolve only the locally configured launcher, never the receipt's engine path.
engine = shutil.which('godot')
require(engine and shared.sha(engine) == start['engine_sha256']
== manifest['engine_sha256'] == files['engine.bin']['sha256'],
'current native engine and retained bytes pinned')
derived_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
require(read('parse-paths.json') == parse_paths == derived_paths
and manifest['parse_checks'] == len(derived_paths),
'all exact source parse paths and receipt count')
require(end['passed'] is True and end['engine_ended'] is True and end['error'] is None
and end['cleanup_errors'] == [] and end['guard_errors'] == [],
'finished successful collection without guard/cleanup error')
for receipt in (start, end):
require(receipt['formal_acceptance'] is False and receipt['original_unit_complete'] is False,
'backend evidence never completes original unit')
before = start['default_player_before']
require(before == end['default_player_after'] and before['logs_and_locks_included'] is True
and type(before['existed']) is bool and type(before['files']) is dict,
'complete default player before/after equality')
for name, pin in before['files'].items():
confined(out, name) # Validate the inventory name; never open the player.
require(set(pin) == {'bytes', 'sha256'} and type(pin['bytes']) is int
and pin['bytes'] >= 0 and re.fullmatch('[0-9a-f]{64}', pin['sha256']),
'whole default player file pin')
require(before['existed'] or before['files'] == {}, 'absent player has no files')
native = shared.module('p05_verify_native', shared.SUPPORT / 'owned_diagnostic_process.py')
diagnostics = shared.module('p05_verify_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
owners = shared.module('p05_verify_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
# Helpers are loaded for their pure gates only. No run/probe/cleanup is called.
origin = Path(start['stage']).parent
require(Path(start['stage']) == origin / 'stage' and origin.is_absolute()
and '..' not in origin.parts, 'original output identity')
def relocated(value):
if isinstance(value, dict):
return {relocated(k): relocated(v) for k, v in value.items()}
if isinstance(value, list):
return [relocated(v) for v in value]
if isinstance(value, str) and value.startswith(str(origin) + '/'):
return str(confined(out, value[len(str(origin)) + 1:]))
return value
def proof_read(path):
path = plain_path(path)
require(path.is_relative_to(out), 'proof path stays in output')
return relocated(read(path.relative_to(out).as_posix()))
owners.read = proof_read
phases = end['phases']
require(len(phases) == len(SUITES) + 2, 'exact import, parse, roster and restart phases')
tests = read('all-tests.json')
stage_files = shared.inventory(confined(out, 'stage'), stage=True)
require(set(stage_files) == set(source) | {HANDOFF}, 'retained stage is the source and the restart handoff')
handoff = stage_files[HANDOFF]
require(len(tests) == len(SUITES) and end['all_tests_sha256'] == files['all-tests.json']['sha256'],
'complete test rows')
pids = []
for index, (phase, test) in enumerate([('import', None), ('parse', None)]
+ [(str(i).zfill(3), n) for i, n in enumerate(SUITES)]):
row = read(phase + '-receipt.json')
expected_row = dict(row)
if test:
expected_row.update(sequence=index - 2, godot=start['engine'])
require(tests[index - 2] == expected_row, 'test row matches raw phase receipt')
require(phases[index] == expected_row and row['phase'] == phase and row['test'] == test
and row['passed'] is True and row['failed'] is False
and row['validation_error'] is None and not row.get('guard_error')
and row['default_player_entire_profile_unchanged'] is True,
'exact successful phase')
pid = row['actual_pid']
require(type(pid) is int and pid > 0 and pid not in pids, 'distinct actual PIDs per phase')
pids.append(pid)
process = read(phase + '/process.json')
require(process == row['native_process'] and process['pid'] == pid
and process['owned_child_process_helper_sha256'] == native.HELPER_SHA
== source['tools/regression/p09_owned_engine_process.py']['sha256'],
'actual phase/process/helper agreement')
stdout, stderr, logger = (text(phase + '/' + n) for n in
('stdout.log', 'stderr.log', 'engine-native.log'))
raw = stdout + stderr
require(text(row['log']) == raw and row['log_sha256'] == files[row['log']]['sha256'],
'literal complete raw stream')
require(process['stdout_sha256'] == files[phase + '/stdout.log']['sha256']
and process['stderr_sha256'] == files[phase + '/stderr.log']['sha256'],
'process transport SHA')
require(row['raw_native_log'] == dict(files[phase + '/engine-native.log'],
path=str(origin / phase / 'engine-native.log')), 'native logger pin')
review = diagnostics.review_streams(native, {
'stdout': stdout, 'stderr': stderr, 'native_file': logger}, test)
require(row['finite_original_diagnostic_review'] == review
and diagnostics.diagnostic_result_gate(process, review)
and shared.process_gate(process, review['allowed_e31_error_lines']),
'strict diagnostics/process gates')
require(native.native_diagnostics(stdout, stderr, row['command'])
== process['strict_native_diagnostics'], 'raw banner/diagnostics recomputed')
command = [start['engine'], '--headless', '--path', start['stage']]
if phase == 'import':
command += ['--editor', '--import', '--quit', '--verbose']
else:
script = ('bootstrap/parse_full_dependencies.gd' if phase == 'parse'
else 'starters/' + phase + '.gd')
command += ['--script', str(origin / script), '--verbose']
require(row['starter_sha256'] == files[script]['sha256'], 'exact starter pin')
if test:
expected_starter = ('extends ' + json.dumps('res://' + test) + '\nconst Guard := preload('
+ json.dumps(str(origin / 'bootstrap/profile_guard.gd'))
+ ')\nfunc _initialize() -> void:\n\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n')
require(text(script) == expected_starter, 'unchanged guarded starter bytes')
command += ['--log-file', str(origin / phase / 'engine-native.log')]
require(row['command'] == process['argv'] == command, 'exact native argv/logger')
for filename, field, event in [('started.json', 'actual_start_binding', 'ENGINE_STARTED'),
('ended.json', 'actual_end_binding', 'ENGINE_ENDED')]:
binding = read(phase + '/' + filename)
require(binding == process[field] and binding['event'] == event
and binding['actual_pid'] == binding['pid'] == pid
and binding['owner_pid'] == start['owner_runner_pid']
and binding['command'] == binding['argv'] == command, 'actual START/END ownership')
binding = process['actual_start_binding']
identity = binding['current_process_identity']
if identity is None:
require(binding['tracked_child_ended_before_initial_probe'] is True, 'early ended tracked child')
else:
require(binding['live_identity_exact'] is True and identity == {
'pid': pid, 'ppid': start['owner_runner_pid'], 'pgid': pid, 'args': ' '.join(command)},
'recorded live child PID/PPID/PGID/argv')
finish = process['actual_end_binding']
require(finish['engine_ended'] is True and finish['timed_out'] is False
and finish['lifecycle_error'] is None and finish['exit_code'] == 0
and finish['actual_process_exit_code'] == 0
and finish['start_receipt_written'] is True
and finish['monotonic_ns'] >= binding['monotonic_ns'], 'complete native END')
cleanup = read(phase + '/adapter-owned-cleanup.json')
require(cleanup == process['adapter_owned_cleanup'], 'raw adapter cleanup')
cleanup_gate(cleanup)
capture = row['outer_actual_capture']
require(read(phase + '/outer-owned-finally.json') == {
k: v for k, v in capture.items()
if k not in ('outer_receipt_written', 'outer_receipt_error', 'passed')},
'whole raw outer receipt')
require(capture['actual_captured_pids'] == [pid] and capture['passed'] is True
and capture['all_captured_ended'] is True and capture['adapter_exception'] is None
and capture['outer_cleanup_errors'] == [] and capture['outer_receipt_error'] is None
and capture['outer_receipt_written'] is True
and capture['all_cleanup_before_outer_receipt'] is True
and capture['foreign_process_or_group_signals'] == 0
and row['engine_ended'] is True and row['exit_code'] == 0, 'owned outer capture')
outer_cleanup(capture['cleanup'], [pid])
startup = read(phase + '-startup-profile-before.json')
require(startup == {'path': row['whole_startup_profile_after']['path'], 'existed': False,
'files': {}, 'before_native_spawn': True}, 'fresh startup profile')
require(row['whole_startup_profile_after']['fresh_before_engine'] is True,
'startup fresh before engine')
name = row['profile_startup_name']
require(re.fullmatch('TheTree-PCK-tests-[0-9a-f]{32}-standard-startup-' + phase, name)
and Path(startup['path']).name == name and row['private_project_fields'] == 2
and row['private_project_sha256'] == hashlib.sha256(shared.private_project(
(shared.HERE.parent / 'project.godot').read_bytes(), name)).hexdigest(),
'exact two-field startup isolation')
for label in ('startup', 'runtime'):
if label == 'runtime' and phase == 'import':
continue
whole = row['whole_' + label + '_profile_after']
archive = confined(out, 'profiles/' + phase + '/' + label)
require(archive.is_dir() and shared.inventory(archive)
== whole['files'], 'complete retained private profile')
if phase != 'import':
guard = read(phase + '-profile.json')
runtime = Path(row['whole_runtime_profile_after']['path'])
name = guard['custom_user_dir_name']
require(re.fullmatch('TheTree-PCK-tests-[0-9a-f]{32}-standard-runtime-' + phase, name)
and runtime.name == name and guard['verified'] is True
and guard['conditions'] == [True] * 5 and guard['conditions_checked'] == 5
and guard['fresh_before_parent_initialize'] is True
and guard['profile_writes_before_guard'] == 0
and guard['actual_user_data_dir'] == guard['expected_user_data_dir'] == str(runtime)
and guard['globalized_probe'] == str(runtime / 'guard-probe')
and guard['project_settings_persisted'] is False, 'all unchanged Guard conditions')
require(row['actual_private_profile'] == {
'report': guard, 'report_sha256': files[phase + '-profile.json']['sha256'],
'whole_files': row['whole_runtime_profile_after']['files']}, 'whole raw Guard report')
require(row['controlled_environment'] == {
'THE_TREE_PACK_PROFILE_NAME': name, 'THE_TREE_PACK_PROFILE_EXPECTED': str(runtime),
'THE_TREE_PACK_PROFILE_REPORT': str(origin / (phase + '-profile.json')),
'THE_TREE_FULL_PARSE_PATHS': str(origin / 'parse-paths.json'),
**({'THE_TREE_AUDIO_TEARDOWN_PROOF_DIR': str(origin / (phase + '-teardown/owned-audio')),
'THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF': str(origin / (phase + '-teardown/control.json'))}
if test and policy['suite_policies'].get(test, {}).get('ordinary_cleanup_prepared') else {})},
'exact controlled Guard/proof environment')
else:
require(row['controlled_environment'] == {}, 'fresh import environment')
if phase == 'parse':
require(len(re.findall(r'(?m)^FULL_DEPENDENCY_PARSE\b', raw)) == 1
and re.findall(r'(?m)^FULL_DEPENDENCY_PARSE checks=([0-9]+) failures=([0-9]+)\n', raw)
== [(str(len(parse_paths)), '0')] and row['actual_parse_checks'] == len(parse_paths), 'unique all-script parse marker')
if test:
parsed = markers.parse_raw(test, raw, contracts, source[test]['sha256'])
require(parsed['passed'] and row['completion'] == parsed
and row['reported_checks'] == parsed['checks']
and row['reported_failures'] == parsed['failures']
and row['completion_marker'] == parsed['marker'] and row['collector_errors'] == [],
'source-pinned raw completion marker')
require(row['source_sha256'] == row['source_sha256_end'] == source[test]['sha256'],
'exact test source before/after')
p = shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies'])
require(row['policy'] == p, 'current exact suite policy')
if p['ordinary_cleanup_prepared']:
proof = owners.ordinary_gate(confined(out, phase + '-teardown'), pid, out,
p['ordinary_control_labels'])
require(proof == relocated(row['ordinary_owner_pairs']), 'full ordinary/audio ownership proof')
if test == RESTART[0]:
require(row['generated_reports'] == {HANDOFF: dict(handoff, actual_engine_pid=pid,
producer_test=test, producer_source_sha256=source[test]['sha256'], payload_modified=False)},
'the writer generates exactly the retained restart handoff')
require(re.findall(WRITER_LINE, raw) == [(str(pid), handoff['sha256'])]
and read_json(confined(out, 'stage/' + HANDOFF))['writer_pid'] == pid,
'the writer engine printed and recorded its own handoff')
writer_pid = pid
else:
require(row['generated_reports'] == {}, 'only the restart writer generates a report')
if test == RESTART[1]:
require(re.findall(READER_LINE, raw) == [(str(pid), str(writer_pid), handoff['sha256'])]
and pid != writer_pid, 'a separate reader engine read the exact writer handoff')
require(end['actual_captured_PIDs'] == pids, 'all captured engines')
outer_cleanup(end['final_owned_cleanup'], pids)
require(all(end[k] == v for k, v in shared.totals(tests, SUITES).items()), 'recomputed exact totals')
require(shared.inventory(confined(out, 'stage'), stage=True) == dict(source, **{HANDOFF: handoff}),
'entire retained source/settings and handoff exact')
for name, pin in shared.read(shared.SUPPORT / 'bootstrap-scripts.json').items():
require(text('bootstrap/' + name).encode() == pin['text'].encode()
and files['bootstrap/' + name]['sha256'] == pin['sha256'], 'unchanged bootstrap/Guard pins')
require(inventory(out) == dict(files, **{'receipt.json': receipt_pin}),
'whole raw evidence unchanged during verification')
require(content_map(shared.source_refs(shared.HERE.parent)) == source,
'whole current source unchanged during verification')
print(json.dumps({'verified': True, 'actual_suites': len(SUITES), 'parse_checks': len(parse_paths),
'actual_checks': end['actual_all_roster_checks'], 'engines_started': 0,
'formal_acceptance': False, 'original_unit_complete': False}))
return 0
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
# No argparse abbreviation, repeated flags, = aliases or positional tokens.
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument('--output', required=True)
modes = parser.add_mutually_exclusive_group()
modes.add_argument('--collect', action='store_true', help='Root only; fresh native collection')
modes.add_argument('--prepare-only', action='store_true')
seen = set()
i = 0
while i < len(argv):
flag = argv[i]
require(flag in ('--output', '--collect', '--prepare-only', '--help')
and flag not in seen, 'unknown, duplicate or malformed flag')
seen.add(flag)
if flag == '--output':
i += 1
require(i < len(argv) and argv[i] and not argv[i].startswith('--'), 'output value required')
i += 1
args = parser.parse_args(argv)
args.timeout = 1200
root = shared.HERE.parent
output = args.output
# The registered validation passes the literal '{build_dir}/p05-funds-evidence'. The current Runner
# replaces only {python} in validation argv and gives the task's build directory as BUILD_OUTPUT_DIR.
if output.startswith('{build_dir}/'):
build = os.environ.get('BUILD_OUTPUT_DIR', '')
require(build and Path(build).is_absolute() and Path(build).is_dir() and not Path(build).is_symlink(),
'registered build directory from BUILD_OUTPUT_DIR')
output = build + output[len('{build_dir}'):]
out = plain_path(output)
refs = shared.source_refs(root)
require(set(SUITES).issubset(shared.roster(root)), 'normal discovery includes narrow roster and restart tests')
discovered = shared.roster(root)
require(discovered.index(RESTART[0]) < discovered.index(RESTART[1]), 'the full roster also runs the writer first')
markers = shared.module('p05_exact_markers', shared.HERE / 'strict_test_markers.py')
contract_path = root / 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
contract_sha = shared.sha(contract_path)
contracts = markers.load_contracts(contract_path)
require(all(name in contracts for name in SUITES), 'complete marker roster')
policy = shared.read(shared.SUPPORT / 'suite-policy.json')
shared.policy_coverage(SUITES, refs, policy)
for test in SUITES:
shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies'])
require(test not in policy['outside_routes'], 'no outside report routes in the narrow run')
require(policy['fixed_generated_report_routes'].get(test, []) == ([HANDOFF] if test == RESTART[0] else []),
'only the restart writer declares one generated report')
for test in ROSTER[:1] + RESTART:
require(contracts[test]['source_sha256_at_schema_audit'] == refs[test]['sha256'],
'new strict source-hashed marker')
parse_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
if args.prepare_only:
ancestor = out.parent
while not ancestor.exists():
ancestor = ancestor.parent
free = shutil.disk_usage(ancestor).free
size = sum(p['bytes'] for p in refs.values())
print(json.dumps({'roster': ROSTER, 'source_files': len(refs), 'source_bytes': size,
'free_bytes': free, 'conservative_required_bytes': size + shared.MIN_SPACE,
'capacity_available': free >= size + shared.MIN_SPACE, 'parse_paths': len(parse_paths),
'restart_phases': RESTART, 'future_native_phases': len(SUITES) + 2, 'actual_checks': None, 'engines': 0, 'stages': 0,
'formal_acceptance': False}))
return 0
if args.collect:
require(not out.exists() and out.parent.is_dir(), 'fresh output with existing parent required')
require(not out.is_relative_to(root), 'private collection outside source tree')
return collect(args, root, out, refs, markers, contracts, policy,
contract_path, contract_sha, parse_paths)
return verify(out, refs, markers, contracts, policy, contract_sha, parse_paths)
if __name__ == '__main__':
try:
raise SystemExit(main())
except (AssertionError, OSError, ValueError, KeyError, TypeError) as error:
print(type(error).__name__ + ': ' + str(error), file=sys.stderr)
raise SystemExit(1)