Compare commits
6
Commits
613b9886da
...
48d3b0023b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48d3b0023b | ||
|
|
cb183fd127 | ||
|
|
8954914564 | ||
|
|
d65e3da9f3 | ||
|
|
37deb6e877 | ||
|
|
7808ca76db |
+249
-14
@@ -15,6 +15,8 @@ const STARTER_MANA_OUTPUT := 100
|
||||
const STARTER_MANA_SOURCE := &"world_tree.mana_source"
|
||||
const STARTER_MANA_BUS := &"world_tree.mana_bus"
|
||||
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 OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE"
|
||||
const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE"
|
||||
@@ -909,6 +911,9 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
return false
|
||||
if not FacilityUpkeepSettlement.prepare_accounts(restored_economy) or not restored_upkeep.start_at(restored_minute)[&"ok"]:
|
||||
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)
|
||||
if not saved_resources is Dictionary \
|
||||
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
|
||||
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)
|
||||
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:
|
||||
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:
|
||||
@@ -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)
|
||||
if not bool(prepared.get(&"ok", false)):
|
||||
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))
|
||||
if not bool(result[&"ok"]):
|
||||
return result
|
||||
@@ -2524,10 +2539,29 @@ func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dicti
|
||||
if not bool(prepare_hook.get(&"ok", false)):
|
||||
return prepare_hook
|
||||
var day := int(payload[&"day"])
|
||||
var boundary_minute := int(payload[&"boundary_minute"])
|
||||
var preview := staff_service.preview_daily_payroll(day)
|
||||
if not bool(preview.get(&"ok", false)) \
|
||||
or not preview.get(&"rows", null) is Array:
|
||||
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 remaining_gold := available_gold
|
||||
var payments: Dictionary = {}
|
||||
@@ -2592,6 +2626,17 @@ func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dicti
|
||||
if not bool(upkeep_result[&"ok"]):
|
||||
return upkeep_result
|
||||
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(
|
||||
int(payload[&"period_start_minute"]),
|
||||
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))
|
||||
if arrears_total < 0 \
|
||||
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 not bool(economy_service.validate_ledger().get(&"ok", false)) \
|
||||
or not bool(staff_service.validate_invariants().get(&"ok", false)):
|
||||
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)
|
||||
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 {
|
||||
&"ok": true,
|
||||
&"category_totals": financials[&"category_totals"],
|
||||
@@ -2647,6 +2697,8 @@ func _build_operations_daily_financials(
|
||||
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
|
||||
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
|
||||
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT",
|
||||
FinancePolicy.TOP_UP_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY,
|
||||
FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY,
|
||||
]
|
||||
var category_mapping := {
|
||||
&"SERVICE_SALE": &"revenue",
|
||||
@@ -2660,6 +2712,8 @@ func _build_operations_daily_financials(
|
||||
&"EVENT_COST_COMMITTED": &"cost_of_goods",
|
||||
&"DEFENSE_SKILL_COST": &"cost_of_goods",
|
||||
&"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_REPAID": &"financing_principal_outflows",
|
||||
}
|
||||
@@ -3551,8 +3605,32 @@ func _operations_payroll_crosslinks_are_valid(
|
||||
summary_settlement.erase(&"upkeep_arrears_total")
|
||||
elif upkeep_debt != 0:
|
||||
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)) \
|
||||
!= 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:
|
||||
return false
|
||||
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"]))
|
||||
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()
|
||||
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.
|
||||
@@ -3626,15 +3707,20 @@ func repay_loan_principal(
|
||||
return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "The loan liability does not exist.")
|
||||
var remaining := -economy_service.get_balance(liability)
|
||||
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.")
|
||||
# The single balanced posting is atomic. Defer receipts until the Gold
|
||||
# mirror is synchronized, also respecting any enclosing effect scope.
|
||||
# A09: what the front fund cannot pay comes from the hidden fund in the same
|
||||
# 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()
|
||||
var result := economy_service.transfer(
|
||||
PLAYER_CASH_ACCOUNT, liability, amount, _runtime_event_minute(),
|
||||
&"LOAN_PRINCIPAL_REPAID", loan_id, idempotency_key
|
||||
)
|
||||
var result := economy_service.post(entries, idempotency_key)
|
||||
if result.is_success():
|
||||
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
|
||||
result.details[&"loan_id"] = loan_id
|
||||
@@ -3644,6 +3730,144 @@ func repay_loan_principal(
|
||||
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:
|
||||
if economy == null:
|
||||
return false
|
||||
@@ -3697,6 +3921,8 @@ func _operations_expected_category_totals(
|
||||
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
|
||||
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
|
||||
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT",
|
||||
FinancePolicy.TOP_UP_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY,
|
||||
FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY,
|
||||
]
|
||||
var mapping := {
|
||||
&"SERVICE_SALE": &"revenue", &"REFUND": &"refunds",
|
||||
@@ -3707,6 +3933,8 @@ func _operations_expected_category_totals(
|
||||
&"EVENT_COST_COMMITTED": &"cost_of_goods",
|
||||
&"DEFENSE_SKILL_COST": &"cost_of_goods",
|
||||
&"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_REPAID": &"financing_principal_outflows",
|
||||
}
|
||||
@@ -5254,6 +5482,7 @@ func _initialize_economy() -> void:
|
||||
facility_upkeep = FacilityUpkeepAccrual.new()
|
||||
upkeep_settlement = FacilityUpkeepSettlement.new()
|
||||
FacilityUpkeepSettlement.prepare_accounts(economy_service)
|
||||
FinancePolicy.prepare_accounts(economy_service)
|
||||
facility_upkeep.start_at(_clock.absolute_minute)
|
||||
mana_reserve = ManaReserveCycle.new()
|
||||
mana_reserve.start_at(_clock.absolute_minute)
|
||||
@@ -11960,6 +12189,12 @@ func _apply_service_transaction(
|
||||
if service.price_snapshot > 0:
|
||||
var payer_account := _guest_wallet_account(request.guest_ids[0])
|
||||
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):
|
||||
inventory_service.restore_snapshot(inventory_before)
|
||||
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
|
||||
),
|
||||
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
|
||||
),
|
||||
]
|
||||
|
||||
@@ -3925,6 +3925,39 @@
|
||||
"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",
|
||||
|
||||
@@ -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 が照合する。
|
||||
+51
@@ -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 の分母は増やさない。
|
||||
+526
@@ -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
|
||||
}
|
||||
+30
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",
|
||||
"performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762",
|
||||
"progress_contract.json": "ccbdfce8c8365d5c13f59531955708834cce45acd0e136561c95295dfdb5df77",
|
||||
"progress_snapshot.json": "b90224aa67f82882fb5c5d52b1fd22065599bf3ab155f28e97670d84a64ca563",
|
||||
"progress_snapshot.json": "38777ebda6729c8127fcca0eb6ecf133673445b8d66ebb51b44cad00e5457b1a",
|
||||
"progress_validator_guard_checks.json": "72ee513fcf1cad85c24bfd0a4b472f425f6acf117928ece0f6b41e29018da627",
|
||||
"progress_validator_guard_checks.r01.json": "14c26adffa5a1938241db4912e50ea54b660641ee6fee2e412d467dce14d0519",
|
||||
"requirement_coverage.json": "03b10e4da367579db502d0671a6e0dda36fd385111c6f1c0084acaff7605f593",
|
||||
@@ -253,6 +253,6 @@
|
||||
"validate_facility_value_contracts.py": "91640e2a76cd6cc9186f7b85c0bf379b24b6cac0d48962815235ee628e7ae43e",
|
||||
"validate_plan.py": "dc65987db520df75a9b90037eae82819ba4c8863354340681092703454cdfb31",
|
||||
"validator_update_checks.json": "f997d0f3de2cea29550840b1d5cb23647d64f81329de46758551e2b6c8fecc27",
|
||||
"work_packages.json": "445870757a77382bb8591ac4f333838a4db45203e02f131b497ca86d2cbe3dfa"
|
||||
"work_packages.json": "f543f39842764076b4522cc818c476d18bdc8e09ac9d83e02dd84ccadc966496"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"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は元証拠に保持。",
|
||||
"integrated_at_jst": "2026-10-23T22:51:54.402572+09:00",
|
||||
"scope": "原30パッケージ/88成果、正式完了24成果・4パッケージ。部分点なし。P05-02全条件を独立レビュー/通常final/実DB結合で正式完了(P05 packageはP05-03未完)。ユーザー指示により機能実装の完了を優先し、P28-02の最適化と試験速度の調整は後段。全ゲーム/同一Windows最終受入は未完。",
|
||||
"source_head": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
|
||||
"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": {
|
||||
"baseline.json": {
|
||||
"sha256": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b",
|
||||
@@ -160,8 +160,8 @@
|
||||
"in_progress": 26
|
||||
},
|
||||
"current_delivery_state_counts": {
|
||||
"completed": 23,
|
||||
"in_progress": 18,
|
||||
"completed": 24,
|
||||
"in_progress": 17,
|
||||
"planned": 47
|
||||
},
|
||||
"completed_current_deliveries": [
|
||||
@@ -172,6 +172,7 @@
|
||||
"P04-02",
|
||||
"P04-03",
|
||||
"P05-01",
|
||||
"P05-02",
|
||||
"P06-01",
|
||||
"P06-02",
|
||||
"P06-03",
|
||||
@@ -5306,7 +5307,7 @@
|
||||
"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_completed": 23,
|
||||
"original_completed": 24,
|
||||
"original_total": 88,
|
||||
"new_optimization_accepted": false,
|
||||
"current_support_stage": {
|
||||
@@ -5323,7 +5324,8 @@
|
||||
"sha256": "de4adb29bb8a2ec6f2426bdb4c23d5094bdfea764eb045ef33af4948625a45d8"
|
||||
},
|
||||
"original_credit": 0
|
||||
}
|
||||
},
|
||||
"deferred_by_user_feature_first_instruction": true
|
||||
},
|
||||
"current_P14_01_contract_audit": {
|
||||
"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",
|
||||
"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": {
|
||||
"formally_closed": "P28-01",
|
||||
"formally_closed": "P05-02",
|
||||
"next_candidate": "P28-02",
|
||||
"formal_count": 23,
|
||||
"formal_count": 24,
|
||||
"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."
|
||||
"selection_status": "P05-02正式完了。ユーザー指示により機能実装を優先し、回答待ちに依らない次の機能実装をRootの通常登録へ提案する。P28-02の記録は保持し後段。",
|
||||
"pending_decisions": "人への問いの回答待ち: P05-03の3件、P07-01の天候、P16-01-DISMISSの退職金。どれも推定しない。"
|
||||
},
|
||||
"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",
|
||||
@@ -10598,7 +10600,7 @@
|
||||
},
|
||||
"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": {
|
||||
"at": "2026-10-17T20:44:25.180745+09:00",
|
||||
"original_plan_id": "P26-01",
|
||||
@@ -10788,16 +10790,24 @@
|
||||
"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"
|
||||
"project": "the-tree-p05-02",
|
||||
"task": "P05-02",
|
||||
"state": "completed",
|
||||
"submission": "895491456477b42a74e59fa239bea86c4fb9aa33",
|
||||
"candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
|
||||
"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,
|
||||
"original_completed": 23,
|
||||
"original_total": 88
|
||||
"independent_reviewer_session": "local_cbdd640f-21ad-4f21-8d71-12b8debccc21",
|
||||
"independent_result_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
|
||||
"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": {
|
||||
"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",
|
||||
"bytes": 23096,
|
||||
"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",
|
||||
"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": [
|
||||
{
|
||||
"id": "P05-01",
|
||||
@@ -5569,7 +5569,7 @@
|
||||
"title": "全費目と資金区分",
|
||||
"work": "表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税を帳簿へ接続する。",
|
||||
"acceptance": "3営業日で資金増減と台帳が一致し、借入元本を売上に含めない。",
|
||||
"state": "in_progress",
|
||||
"state": "completed",
|
||||
"evidence": [
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p05-02-current-condition-audit-r02/condition-matrix.json",
|
||||
@@ -5642,8 +5642,35 @@
|
||||
"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",
|
||||
"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": "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": [],
|
||||
"updated_at_jst": "2026-10-23T22:51:54.402572+09:00",
|
||||
"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。",
|
||||
"historical_before_formal_cb183fd": {
|
||||
"state": "in_progress",
|
||||
"progress_summary": "次の優先候補。P04はb1810ff4で正式受入済み。全費目・表裏資金・3営業日照合・元本売上外という原条件は維持。既存1299時点の有限証拠と表示候補は履歴として保持し、現版への適用は未監査。D04・資金区分/移行/支払規約の未確定値を捏造せず、実Opusによる次項目選定と既存Runner/成果監査を待つ。新規実装は未起動。",
|
||||
"remaining_gaps": [
|
||||
"表金/裏金の初期5000G・旧Gold割当、費目別支払元・不足時規約を確定し、記帳/保存移行/通常UIを接続する。",
|
||||
@@ -5651,8 +5678,53 @@
|
||||
"全費目/全資金区分の3営業日全資金増減・台帳照合、元本売上外、適用する不足/失敗/再送境界を代表シナリオで検証する。",
|
||||
"借入あり2締め2施設の通常Viewport/折返し/スクロール/元本操作部は有限確認済み。関連金融変更を固めた正確な最終候補で必須全回帰と独立採用/全条件受入を行い正式台帳へ反映する。"
|
||||
],
|
||||
"updated_at_jst": "2026-10-19T00:54:23.322453+09:00",
|
||||
"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。"
|
||||
"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",
|
||||
@@ -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.",
|
||||
"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": {
|
||||
"current1285_candidate_checkpoint": {
|
||||
"at_jst": "2026-10-07T23:54:31.996054+09:00",
|
||||
@@ -6245,7 +6317,8 @@
|
||||
"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_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",
|
||||
|
||||
@@ -65,6 +65,33 @@ func settle(period: Dictionary, economy: EconomyService) -> Dictionary:
|
||||
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:
|
||||
var low := 0
|
||||
var high := _receipts.size()
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://l14tq0avw0ux
|
||||
@@ -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
|
||||
var frozen := PurchaseQuoteV2.make(item_id, quantity, int(item[&"unit_price"]))
|
||||
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)
|
||||
@warning_ignore("integer_division")
|
||||
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:
|
||||
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 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}
|
||||
@@ -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"
|
||||
if quote.has(&"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)
|
||||
if not paid.is_success():
|
||||
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.economy_service.restore_snapshot(economy_before)
|
||||
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.economy_service.restore_snapshot(economy_before)
|
||||
if record_auto_attempt:
|
||||
@@ -274,7 +283,7 @@ func get_report(runtime: Variant) -> Dictionary:
|
||||
row[&"reason_codes"].append(&"NO_INPUT")
|
||||
destinations.append(row)
|
||||
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:
|
||||
|
||||
@@ -76,9 +76,17 @@ func get_report() -> Dictionary:
|
||||
var construction_outflow := 0
|
||||
var wage_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():
|
||||
if entry.absolute_minute < start or entry.absolute_minute > minute or entry.category == &"INITIALIZATION":
|
||||
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":
|
||||
inflow += entry.amount
|
||||
if entry.credit_account == &"PLAYER_CASH":
|
||||
@@ -89,6 +97,10 @@ func get_report() -> Dictionary:
|
||||
upkeep_outflow += entry.amount
|
||||
elif entry.category == &"WAGE":
|
||||
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] = []
|
||||
if _runtime.operations_coordinator != null:
|
||||
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,
|
||||
&"upkeep_cash_out": upkeep_outflow,
|
||||
&"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),
|
||||
}
|
||||
|
||||
@@ -117,6 +134,8 @@ func refresh() -> void:
|
||||
_refresh_repayment_controls()
|
||||
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"]]
|
||||
# 表金は上の手持ち。裏金は裏のサービスの売上で、表金で払えない分だけを補う(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"]
|
||||
if summary.is_empty():
|
||||
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"]:
|
||||
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 "差額あり"]
|
||||
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"):
|
||||
_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"])):
|
||||
@@ -206,7 +230,7 @@ func _build_repayment_controls(style_button: Callable) -> void:
|
||||
loan_amount = LineEdit.new()
|
||||
loan_amount.name = "LoanRepaymentAmount"
|
||||
loan_amount.placeholder_text = "返済する元本(整数G)"
|
||||
loan_amount.tooltip_text = "残元本と手持ちを超えない整数額を入力"
|
||||
loan_amount.tooltip_text = "残元本と表金・裏金の合計を超えない整数額を入力"
|
||||
_loan_box.add_child(loan_amount)
|
||||
loan_amount.text_changed.connect(func(_text: String) -> void: _refresh_repayment_controls())
|
||||
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 remaining := int(row.get(&"outstanding_principal", 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 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 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_all_button.disabled = not allowed or remaining > cash
|
||||
repay_button.disabled = not allowed or amount <= 0 or amount > remaining or amount > funds
|
||||
repay_all_button.disabled = not allowed or remaining > funds
|
||||
loan_amount.editable = allowed
|
||||
if not bool(report.get(&"available", false)) or not bool(report.get(&"ledger_valid", false)):
|
||||
_loan_hint.text = "返済の帳簿を確認できません。"
|
||||
@@ -279,13 +305,13 @@ func _refresh_repayment_controls() -> void:
|
||||
elif loans.is_empty():
|
||||
_loan_hint.text = "借入はありません。"
|
||||
elif waiting:
|
||||
_loan_hint.text = "返済を受理済みです。次の時間更新で実行し、手持ちと残元本を再確認します。"
|
||||
_loan_hint.text = "返済を受理済みです。次の時間更新で実行し、表金・裏金と残元本を再確認します。"
|
||||
elif remaining == 0:
|
||||
_loan_hint.text = "この借入は完済しています。"
|
||||
else:
|
||||
_loan_hint.text = "手持ち %dGから元本を返済します。残元本 %dG。返済額は日次経費に含まれます。" % [cash, remaining]
|
||||
if remaining > cash: _loan_hint.text += "\n完済には手持ちが不足しています。"
|
||||
if not text.is_empty() and repay_button.disabled: _loan_hint.text += "\n残元本と手持ちを超えない正の整数額を入力してください。"
|
||||
_loan_hint.text = "表金 %dG・裏金 %dG(計 %dG)から元本を返済します。表金で足りない分は裏金から補填します。残元本 %dG。返済額は日次経費に含まれます。" % [cash, funds - cash, funds, remaining]
|
||||
if remaining > funds: _loan_hint.text += "\n完済には表金と裏金の合計が不足しています。"
|
||||
if not text.is_empty() and repay_button.disabled: _loan_hint.text += "\n残元本と表金・裏金の合計を超えない正の整数額を入力してください。"
|
||||
|
||||
|
||||
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 text := loan_amount.text.strip_edges()
|
||||
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)):
|
||||
_set_repayment_result("返済できません。手持ち・残元本・返済待ちを確認してください。", false)
|
||||
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)
|
||||
_refresh_repayment_controls()
|
||||
return
|
||||
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())
|
||||
if result.is_success() and (after.get(&"pending_command_ids", []) as Array).has(int(result.command_id)):
|
||||
_loan_pending_command = int(result.command_id)
|
||||
_set_repayment_result("返済を受理しました。実行時に手持ちと残元本を確認します。", true)
|
||||
_set_repayment_result("返済を受理しました。実行時に表金・裏金と残元本を確認します。", true)
|
||||
else:
|
||||
_show_repayment_result(result)
|
||||
_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)
|
||||
loan_amount.text = ""
|
||||
else:
|
||||
_set_repayment_result("返済を実行できませんでした。手持ちと残元本を確認してください。", false)
|
||||
_set_repayment_result("返済を実行できませんでした。表金・裏金と残元本を確認してください。", false)
|
||||
|
||||
|
||||
func _on_repayment_completed(result: Variant) -> void:
|
||||
|
||||
@@ -300,6 +300,28 @@ func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> D
|
||||
old[&"elevators"] = raised
|
||||
fields.append("elevators_v1_to_v2")
|
||||
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():
|
||||
# Saves keep the Runtime's own key order, so the mapped tree follows it.
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -414,6 +414,29 @@
|
||||
],
|
||||
"old_bounded_teardown": 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": {
|
||||
@@ -448,6 +471,9 @@
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"artifacts/upkeep-2026-09-29/preclose-snapshot.txt",
|
||||
"artifacts/upkeep-2026-09-29/closed-snapshot.txt"
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user