P08-03: 配置の通常操作受入

This commit is contained in:
Agent Control Runner
2026-10-14 16:08:32 +09:00
parent 7e2368f5c4
commit 47750010b8
9 changed files with 498 additions and 56 deletions
@@ -457,3 +457,46 @@ Actual R09 --verify-only: integrity pass, acceptance pending.
- tests/fixtures/p08-03/repair13_images.py: 248a8d73ece3a60e601421fdd2ad53f8f8e971184fb1328c7155a71b8ed4ca53
- repair13-local-validation.json: f249c76c6d4c044c1a3c33ea09abc99f09a8c0ea6b0ade470d2af205eb4d89af
- repair13-local-source-manifest.json: 7d5a62155b7053398a5bc55879c9a92a07dcb3c5a0603e92d6f38eefba122ca1
## Repair17 revision 2 — submitted, no acceptance claimed
History above and all R09 evidence/pins remain historical and unrewritten.
(1) support_connection is NOT satisfied: blocked pending Root reconciliation and application of runtime-preview-proposal/runtime_preview.patch. See proposal for the additional Main display premise gap.
(2)–(5) are not verified. The earlier reviewer raw-proof read refusal remains historical. The reviewer now has the permitted task build via --add-dir (while --restricted, Read/Grep/Glob and outer sandbox remain); future independent checking with raw-proof access is required.
Native/OS proof for changed demolition status and support display is invalidated. Root must recollect after all product fixes, including the reconciled Runtime proposal, are combined. No applicability or acceptance is claimed.
Limits for repair, inference and material remain null. No 500k compression; low findings (empty cell, audio, idempotency, English Runtime messages) remain deferred. Denominator remains one existing unit among 30 packages / 88 units, no partial credit.
New HEAD: to be bound by Runner. No worker commit or branch change.
Validation: 128 Python tests passed (85.953s, exit0) after one targeted-test correction; py_compile passed with an in-scope cache directory after default-cache PermissionError; JSON load and git apply --check passed. placement.gd and proposed GDScript test are not run. Placement fixture requires Root's complete applied fix.
Pending:
- Root reconciliation and application of Runtime preview patch (including Main display premise gap)
- Root native/OS recollection for changed demolition and support UI after all fixes are combined
- Root final full175
- Independent Opus review of all 5 conditions with raw-proof access
- Integration / current final verification / ledger
- Windows same-distribution/device (no Mac substitution)
Net committed diff, from git diff --name-status --no-renames 31aa7706d28a92e313c432d5a62e35d19503dcc3 HEAD:
```text
M docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md
A docs/plans/original-completion-2026-10-02/execution/p08-03/REPAIR12_CONTEXT_GAP.md
A docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_NATIVE_R09_COLLECTION_PINS.json
A docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_R09_COLLECTION_AND_PUBLICATION_DIAGNOSIS.json
A docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_R09_READONLY_IMAGE_DIMENSIONS.json
M docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json
A docs/plans/original-completion-2026-10-02/execution/p08-03/repair13-local-source-manifest.json
A docs/plans/original-completion-2026-10-02/execution/p08-03/repair13-local-validation.json
M tools/check_p08_normal_placement_evidence.py
M tools/test_p08_normal_placement_evidence.py
```
Working changes (plus new proposal files):
```text
M docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md
M docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json
M scenes/main/main.gd
M tests/fixtures/p08-03/checker_fixture.py
M tests/fixtures/p08-03/placement.gd
M tools/check_p08_normal_placement_evidence.py
M tools/test_p08_normal_placement_evidence.py
A docs/plans/original-completion-2026-10-02/execution/p08-03/runtime-preview-proposal/RUNTIME_PREVIEW_PROPOSAL.md
A docs/plans/original-completion-2026-10-02/execution/p08-03/runtime-preview-proposal/runtime_preview.patch
```
Exact validation commands and initial/retry results are recorded in implementation.json repair17.validation. No Godot/native/full execution or recollection was performed.
@@ -64,73 +64,58 @@
"tests/fixtures/p08-03/r05-reader-checkpoint.json.gz": "27fd3b41834106ccd6a0980dfa2bc91cc19806dd3f552ba04246efb400763233"
},
"actual_local_validation": {
"command": "python3 tools/test_p08_normal_placement_evidence.py",
"tests": 116,
"duration_seconds": 80.606,
"command": "python3 -B tools/test_p08_normal_placement_evidence.py",
"tests": 128,
"duration_seconds": 85.953,
"exit_code": 0,
"failures": 0,
"errors": 0,
"skipped": 0,
"focused_command": "python3 tools/test_p08_normal_placement_evidence.py R09ImageTests R09ApplicabilityTests",
"focused_tests": 18,
"focused_exit_code": 0,
"python_ast_checks": 2,
"diff_check": "passed",
"native_OS_full_regression": "not executed by worker",
"actual_R09_verify_only": "not executed by worker",
"original_capture_hashes": "actual original 138 images checked inside pure Python test; Root re-check pending",
"earlier_attempts": [
"101 tests: helper naming errors, corrected",
"18 focused tests: helper naming errors, corrected",
"18 focused tests: passed with one Root-positive skip before actual bytes were used",
"18 focused tests: passed without skips after actual original hashes were used"
]
"scope": "pure Python only; no acceptance",
"all_commands": "repair17.validation"
},
"conditions": {
"support_connection": {
"implementation": "prepared",
"local_checker_controls": "116 pure Python checker tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"actual_native_OS": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"local_checker_controls": "128 pure Python tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "historical only; current native recollection pending",
"actual_native_OS": "invalidated for changed demolition/support display; recollect after combined product fixes",
"independent_review": "pending",
"accepted": false,
"state": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending"
"state": "blocked pending Root reconciliation and application of runtime_preview.patch; Main display premise gap recorded"
},
"invalid_position_reason": {
"implementation": "prepared",
"local_checker_controls": "116 pure Python checker tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"actual_native_OS": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"local_checker_controls": "128 pure Python tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "historical only; current native recollection pending",
"actual_native_OS": "invalidated for changed demolition/support display; recollect after combined product fixes",
"independent_review": "pending",
"accepted": false,
"state": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending"
"state": "not verified; historical raw-proof read refusal retained. Reviewer permitted task build through --add-dir while --restricted, Read/Grep/Glob and outer sandbox remain. Future independent raw-proof check required."
},
"demolish_rebuild": {
"implementation": "prepared",
"local_checker_controls": "116 pure Python checker tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"actual_native_OS": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"local_checker_controls": "128 pure Python tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "historical only; current native recollection pending",
"actual_native_OS": "invalidated for changed demolition/support display; recollect after combined product fixes",
"independent_review": "pending",
"accepted": false,
"state": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending"
"state": "not verified; historical raw-proof read refusal retained. Reviewer permitted task build through --add-dir while --restricted, Read/Grep/Glob and outer sandbox remain. Future independent raw-proof check required."
},
"pan_zoom_world_identity": {
"implementation": "prepared",
"local_checker_controls": "116 pure Python checker tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"actual_native_OS": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"local_checker_controls": "128 pure Python tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "historical only; current native recollection pending",
"actual_native_OS": "invalidated for changed demolition/support display; recollect after combined product fixes",
"independent_review": "pending",
"accepted": false,
"state": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending"
"state": "not verified; historical raw-proof read refusal retained. Reviewer permitted task build through --add-dir while --restricted, Read/Grep/Glob and outer sandbox remain. Future independent raw-proof check required."
},
"separate_pid_resume": {
"implementation": "prepared",
"local_checker_controls": "116 pure Python checker tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"actual_native_OS": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending",
"local_checker_controls": "128 pure Python tests, exit0; no gameplay acceptance",
"actual_Godot_handler": "historical only; current native recollection pending",
"actual_native_OS": "invalidated for changed demolition/support display; recollect after combined product fixes",
"independent_review": "pending",
"accepted": false,
"state": "Root native evidence collected at 31aa; independent semantic acceptance pending; Root final full175 regression pending; integration pending"
"state": "not verified; historical raw-proof read refusal retained. Reviewer permitted task build through --add-dir while --restricted, Read/Grep/Glob and outer sandbox remain. Future independent raw-proof check required."
}
},
"pending": [
@@ -1812,5 +1797,106 @@
"preview/runtime stair difference": "open",
"coordinator runtime acceptance=false": "open",
"Windows same-distribution": "open"
},
"repair17": {
"revision": 2,
"direction": "repair17 following review 3124b341",
"status": "submitted; no acceptance claimed",
"head": "to be bound by Runner",
"support_connection": "blocked pending Root reconciliation and application of runtime_preview.patch; Main display premise gap recorded",
"conditions_2_to_5": "not verified; historical raw-proof read refusal retained. Reviewer permitted task build through --add-dir while --restricted, Read/Grep/Glob and outer sandbox remain. Future independent raw-proof check required.",
"R09": "historical pins/evidence unrewritten; current R09 missing_connection must fail preview/command disagreement",
"native_OS": "invalidated for changed demolition/support display; recollect after combined product fixes",
"pending": [
"Root reconciliation and application of Runtime preview patch (including Main display premise gap)",
"Root native/OS recollection for changed demolition and support UI after all fixes are combined",
"Root final full175",
"Independent Opus review of all 5 conditions with raw-proof access",
"Integration / current final verification / ledger",
"Windows same-distribution/device (no Mac substitution)"
],
"repair_inference_material_limits": null,
"validation": [
{
"command": "python3 -B tools/test_p08_normal_placement_evidence.py",
"initial_exit_code": 1,
"initial_tests": 128,
"initial_duration_seconds": 92.811,
"initial_failure": "test_preview_partial_state_change reached snapshot-envelope validation; fixed mutation to existing resource field",
"exit_code": 0,
"tests": 128,
"duration_seconds": 85.953
},
{
"command": "python3 -m py_compile tools/check_p08_normal_placement_evidence.py tools/test_p08_normal_placement_evidence.py tests/fixtures/p08-03/checker_fixture.py",
"initial_exit_code": 1,
"initial_failure": "PermissionError on default external PYTHONPYCACHEPREFIX cache directory",
"retry_environment": {
"PYTHONPYCACHEPREFIX": "tests/fixtures/p08-03/.scratch/repair17-pycache"
},
"retry_exact_command": "PYTHONPYCACHEPREFIX=tests/fixtures/p08-03/.scratch/repair17-pycache python3 -m py_compile tools/check_p08_normal_placement_evidence.py tools/test_p08_normal_placement_evidence.py tests/fixtures/p08-03/checker_fixture.py",
"exit_code": 0,
"cache_cleanup": "removed only worker-generated cache"
},
{
"command": "python3 -c \"import json,sys;json.load(open(sys.argv[1]))\" docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json",
"exit_code": 0
},
{
"command": "git apply --check docs/plans/original-completion-2026-10-02/execution/p08-03/runtime-preview-proposal/runtime_preview.patch",
"exit_code": 0,
"applied": false
}
],
"net_diff_command": "git diff --name-status --no-renames 31aa7706d28a92e313c432d5a62e35d19503dcc3 HEAD",
"net_diff_committed": "M\tdocs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md\nA\tdocs/plans/original-completion-2026-10-02/execution/p08-03/REPAIR12_CONTEXT_GAP.md\nA\tdocs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_NATIVE_R09_COLLECTION_PINS.json\nA\tdocs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_R09_COLLECTION_AND_PUBLICATION_DIAGNOSIS.json\nA\tdocs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_R09_READONLY_IMAGE_DIMENSIONS.json\nM\tdocs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json\nA\tdocs/plans/original-completion-2026-10-02/execution/p08-03/repair13-local-source-manifest.json\nA\tdocs/plans/original-completion-2026-10-02/execution/p08-03/repair13-local-validation.json\nM\ttools/check_p08_normal_placement_evidence.py\nM\ttools/test_p08_normal_placement_evidence.py\n",
"working_changes": "M\tdocs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md\nM\tdocs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json\nM\tscenes/main/main.gd\nM\ttests/fixtures/p08-03/checker_fixture.py\nM\ttests/fixtures/p08-03/placement.gd\nM\ttools/check_p08_normal_placement_evidence.py\nM\ttools/test_p08_normal_placement_evidence.py\n",
"new_files": [
"runtime-preview-proposal/RUNTIME_PREVIEW_PROPOSAL.md",
"runtime-preview-proposal/runtime_preview.patch"
],
"applied_files": [
"scenes/main/main.gd",
"tools/check_p08_normal_placement_evidence.py",
"tools/test_p08_normal_placement_evidence.py",
"tests/fixtures/p08-03/checker_fixture.py",
"tests/fixtures/p08-03/placement.gd",
"docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md",
"docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json"
],
"recorded_direction_context_gap": "main.gd:2123-2131 error-code-first rendering does not display the Runtime message; Root reconciliation required; build renderer not changed"
},
"repair17_local_source_sha256": {
"scenes/main/main.gd": "3b0e9d736a26d848700854e7162b3de9b92ac26fc7e26998e65fe3e95f71d8d6",
"tools/check_p08_normal_placement_evidence.py": "f1e090fdb6f6e505b76e0853a22b6b6f7464f1dbf01b4b831bec361fa8141150",
"tools/test_p08_normal_placement_evidence.py": "1b42cb89ae6d00cb6fe8cb978e95ca6bc0e392912ad3b9e904dc34e29495eb63",
"tests/fixtures/p08-03/checker_fixture.py": "576d5dbeeda669dfda7866fb7eb7a7b3011c00f08e070ca12540fd875ab77acd",
"tests/fixtures/p08-03/placement.gd": "0b50217d9d1ddd0549a428135617a310d95e81a9c4a8a029212019b1dfb19ecb",
"docs/plans/original-completion-2026-10-02/execution/p08-03/runtime-preview-proposal/runtime_preview.patch": "415360b1f22868c2bf862e4cedd5ce4690104054a43874aa983665089c33575b",
"docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md": "0fade567e5a25c3a2373bf1995ffb84f7eaae218532dabccde52bf257f85564e",
"docs/plans/original-completion-2026-10-02/execution/p08-03/runtime-preview-proposal/RUNTIME_PREVIEW_PROPOSAL.md": "fca5c0ded91ff8a17f6b2c98183d7e2991a80b57fcacecf1a5688f61d959892a"
},
"historical_pre_repair17_local_validation": {
"command": "python3 tools/test_p08_normal_placement_evidence.py",
"tests": 116,
"duration_seconds": 80.606,
"exit_code": 0,
"failures": 0,
"errors": 0,
"skipped": 0,
"focused_command": "python3 tools/test_p08_normal_placement_evidence.py R09ImageTests R09ApplicabilityTests",
"focused_tests": 18,
"focused_exit_code": 0,
"python_ast_checks": 2,
"diff_check": "passed",
"native_OS_full_regression": "not executed by worker",
"actual_R09_verify_only": "not executed by worker",
"original_capture_hashes": "actual original 138 images checked inside pure Python test; Root re-check pending",
"earlier_attempts": [
"101 tests: helper naming errors, corrected",
"18 focused tests: helper naming errors, corrected",
"18 focused tests: passed with one Root-positive skip before actual bytes were used",
"18 focused tests: passed without skips after actual original hashes were used"
]
}
}
@@ -0,0 +1,39 @@
# OUTSIDE CURRENT TASK SCOPE — NOT APPLIED — Root reconciles against standing full-game implementation authorization after concrete review; no approval inferred.
## Problem and concrete proposal
At HEAD 7e2368f5c453a3900ba1bb038d7138d25f7eb4fb, guest_stairs at (-8,2,0), without a path at (-8,1,0), has preview can_build=true but submit_command(build_facility) returns VALIDATION_FAILED / STAIR_LANDING_MISSING / 「先に階段の一つ下の階に通路を作ってください。」.
WorldPreview uses ConstructionService alone (scenes/main/world_preview.gd:201-218); Runtime performs additional checks (autoload/simulation_runtime.gd:7576-7615).
runtime_preview.patch is a concrete proposal against this HEAD. It is NOT applied. It changes exactly:
- autoload/simulation_runtime.gd
- scenes/main/world_preview.gd
- tests/transport/test_runtime_build_preview.gd (new)
- tests/transport/test_runtime_build_preview.gd.uid (new)
The private _build_facility_precheck(command_id, payload) returns CommandResult or null. Taking the raw payload rather than eagerly parsed parameters preserves the exact original order: operations gate, definition lookup/NOT_FOUND, unsupported vertical, origin parsing/INVALID_PAYLOAD, stair landing, busy elevator, rotation/INVALID_PAYLOAD. The existing prefix is moved unchanged; execution calls it, then reads the already-validated payload and calls start_construction unchanged. There is no copied domain rule in Main, WorldPreview or the checker.
The public read-only preview_build_facility(definition_id: StringName, origin: Vector3i, rotation: int) runs that helper and merges validate_placement(...).to_dictionary(). A failure forces can_build=false and prepends details.reason (or Code key) plus the exact message. Reasons retain PlacementReport's dictionary shape (code/message/cells/details), not strings. WorldPreview uses the guarded Runtime API, otherwise retains its old fallback, and keeps connection-report augmentation.
## Required reconciliation: recorded Main premise is false
The recorded direction says Main needs no change. Direct source inspection contradicts it: scenes/main/main.gd:2123-2131 selects the first error code and returns 「配置不可: <mapped code>」 before inspecting reason messages. STAIR_LANDING_MISSING is not mapped to the Runtime message. Thus the proposed patch fixes can_build but, by itself, cannot satisfy the specified exact displayed-message equality. No build-status hunk is applied or smuggled into this proposal. Root must reconcile this concrete source gap before applying a complete fix. Condition (1) remains blocked. The green UI is not hidden, recoloured or reworded.
## Purity check (current source; not a native execution)
- autoload/simulation_runtime.gd:7988-8002 _operations_construction_investment_gate reads gate/arrears and constructs a new result only; scripts/operations/operations_coordinator.gd:546-562 reads terminal/arrears/quarantine and returns a fresh dictionary.
- autoload/simulation_runtime.gd:1890-1891 and :4020-4027 sum payroll/upkeep; scripts/staff/staff_service.gd:198 builds a fresh obligations projection; scripts/economy/facility_upkeep_settlement.gd:94-98 sums existing debts.
- autoload/simulation_runtime.gd:6687-6688 _staff_assignment_failure only constructs CommandResult. No command queue, journal, ID allocation or event path is invoked.
- autoload/simulation_runtime.gd:12744-12759 _elevator_shaft_busy/_elevator_touches_module read dictionaries and definition fields. scripts/transport/elevator_service.gd:345-360 get_report duplicates shafts and computes local projection rows.
- scripts/world/path_service.gd:1415-1416 has_cell is a dictionary lookup.
- scripts/construction/construction_service.gd:56-111 validate_placement creates a local PlacementReport; :862-973 fill only that report using read-only world calls; :975-993 checks resource sufficiency. :1136-1144 _can_afford invokes the configured resource query. autoload/game_state.gd:44-48 and scripts/economy/ledger_backed_resource_state.gd:42-48 only read resources.
- scripts/world/world_service.gd:305 onward validate_support builds local structural sets/reports; :122 cells_for_footprint and :194 get_occupant are read queries. scripts/construction/placement_report.gd:36-61 modifies only the new report; :73 to_dictionary returns its projection.
These production dependencies contain no event emission, counters, command-log writes, RNG advance or model mutation in the preview call graph. Mutation begins in ConstructionService.start_construction (:114 onward), which the preview never calls. The helper may return a freshly allocated CommandResult without allocating a command ID; preview never calls submit_command. Purity must also be checked natively through byte-identical typed snapshots after Root applies the proposal; source review is not execution proof.
## Test impact and pending targeted checks
Expected unchanged outcomes: tests/transport/test_runtime_stairs.gd; tests/s1/test_construction_service.gd; tests/mvp/test_original_room_zones.gd; tests/ui/test_main_dashboard.gd; tests/ui/test_pixel_tower.gd. Related fixtures tests/fixtures/p08-03/placement.gd and passive_placement.gd require fresh native evidence after the combined product correction.
The new proposed test asserts:
- failing-cell preview false, STAIR_LANDING_MISSING and exact Runtime message;
- var_to_bytes(create_snapshot()) byte identity before/after preview;
- command reason/message equality at the same cell;
- with diagnostic path at (-8,1,0), preview true and command OK (existing initial reception supplies support);
- operations gate and unsupported-vertical precedence over landing (explicit test doubles isolate ordering, existing domain tests retain real values);
- WorldPreview surfaces the Runtime code.
No GDScript test was run. The patch is checked only with git apply --check. It is a proposal, not acceptance or an applied Runtime change.
@@ -0,0 +1,169 @@
diff --git a/autoload/simulation_runtime.gd b/autoload/simulation_runtime.gd
--- a/autoload/simulation_runtime.gd
+++ b/autoload/simulation_runtime.gd
@@ -7573,18 +7573,33 @@
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, failure_message)
return CommandResult.succeeded(command.command_id, {&"request_id": request_id, &"reason": &"GUEST_LEFT"})
-func _execute_build_facility(command: SimulationCommand) -> CommandResult:
- var operations_gate := _operations_construction_investment_gate(command.command_id)
+func preview_build_facility(definition_id: StringName, origin: Vector3i, rotation: int) -> Dictionary:
+ var failure := _build_facility_precheck(0, {&"definition_id": definition_id, &"origin": origin, &"rotation": rotation})
+ var definition := definition_registry.get_facility(definition_id)
+ var report: Dictionary = construction_service.validate_placement(definition, origin, rotation).to_dictionary()
+ if failure != null:
+ report[&"can_build"] = false
+ var code := String(failure.details.get(&"reason", CommandResult.Code.keys()[failure.code]))
+ var codes: Array = Array(report.get(&"error_codes", []))
+ var reasons: Array = Array(report.get(&"reasons", []))
+ codes.push_front(code)
+ reasons.push_front({&"code": code, &"message": failure.message, &"cells": [], &"details": failure.details.duplicate(true)})
+ report[&"error_codes"] = codes
+ report[&"reasons"] = reasons
+ return report
+
+func _build_facility_precheck(command_id: int, payload: Dictionary) -> CommandResult:
+ var operations_gate := _operations_construction_investment_gate(command_id)
if operations_gate != null:
return operations_gate
- var definition_id := StringName(command.payload.get(&"definition_id", command.payload.get("definition_id", "")))
+ var definition_id := StringName(payload.get(&"definition_id", payload.get("definition_id", "")))
var definition := definition_registry.get_facility(definition_id)
if definition == null:
- return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Facility definition was not found.")
+ return CommandResult.failed(CommandResult.Code.NOT_FOUND, command_id, "Facility definition was not found.")
if _facility_definition_is_unsupported_vertical_proxy(definition):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
- command.command_id,
+ command_id,
"Vertical infrastructure topology is not supported.",
{
&"reason": FacilityInfrastructureProjector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY,
@@ -7592,18 +7607,28 @@
&"unchanged": true,
}
)
- var origin_value: Variant = _parse_world_cell(command.payload.get(&"origin", command.payload.get("origin", null)))
+ var origin_value: Variant = _parse_world_cell(payload.get(&"origin", payload.get("origin", null)))
if origin_value == null:
- return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "origin must be Vector3i or [x, y, layer].")
+ return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command_id, "origin must be Vector3i or [x, y, layer].")
var origin: Vector3i = origin_value
if definition.id in [&"guest_stairs", &"service_stairs"] and not path_service.has_cell(origin + Vector3i(0, -1, 0)):
- return _staff_assignment_failure(command.command_id, &"STAIR_LANDING_MISSING", "先に階段の一つ下の階に通路を作ってください。")
+ return _staff_assignment_failure(command_id, &"STAIR_LANDING_MISSING", "先に階段の一つ下の階に通路を作ってください。")
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
if _elevator_touches_module(shaft, definition, origin) and _elevator_shaft_busy(shaft):
- return _staff_assignment_failure(command.command_id, &"ELEVATOR_BUSY", "Wait until the shaft is idle before extending it.")
+ return _staff_assignment_failure(command_id, &"ELEVATOR_BUSY", "Wait until the shaft is idle before extending it.")
+ var rotation := int(payload.get(&"rotation", payload.get("rotation", 0)))
+ if rotation not in definition.allowed_rotations:
+ return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command_id, "The selected rotation is not allowed.")
+ return null
+
+func _execute_build_facility(command: SimulationCommand) -> CommandResult:
+ var failure := _build_facility_precheck(command.command_id, command.payload)
+ if failure != null:
+ return failure
+ var definition_id := StringName(command.payload.get(&"definition_id", command.payload.get("definition_id", "")))
+ var definition := definition_registry.get_facility(definition_id)
+ var origin: Vector3i = _parse_world_cell(command.payload.get(&"origin", command.payload.get("origin", null)))
var rotation := int(command.payload.get(&"rotation", command.payload.get("rotation", 0)))
- if rotation not in definition.allowed_rotations:
- return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "The selected rotation is not allowed.")
var result := construction_service.start_construction(
definition,
origin,
diff --git a/scenes/main/world_preview.gd b/scenes/main/world_preview.gd
--- a/scenes/main/world_preview.gd
+++ b/scenes/main/world_preview.gd
@@ -202,6 +202,13 @@
var target: Variant = _hovered_cell if cell == null else cell
if build_definition == null or not target is Vector3i:
return INVALID_PREVIEW.duplicate(true)
+ if _runtime != null and _runtime.has_method(&"preview_build_facility"):
+ var result: Dictionary = _runtime.call(&"preview_build_facility", build_definition.id,
+ Vector3i(target.x, target.y, layer_mode), build_rotation)
+ result[&"connection"] = _build_connection_report(
+ _cells_from_serialized(result.get(&"cells", [])),
+ Vector3i(target.x, target.y, layer_mode))
+ return result
var construction: Variant = _service(&"construction_service")
if construction != null and construction.has_method(&"validate_placement"):
var report: Variant = construction.call(
diff --git a/tests/transport/test_runtime_build_preview.gd b/tests/transport/test_runtime_build_preview.gd
new file mode 100644
--- /dev/null
+++ b/tests/transport/test_runtime_build_preview.gd
@@ -0,0 +1,64 @@
+extends SceneTree
+
+var failures := 0
+
+class GatedRuntime extends SimulationRuntimeService:
+ var block_operations := false
+ var block_vertical := false
+ func _operations_construction_investment_gate(command_id: int) -> CommandResult:
+ if block_operations:
+ return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command_id,
+ "Operations currently block construction and investment.", {&"reason": &"ARREARS_REMAIN"})
+ return super._operations_construction_investment_gate(command_id)
+ func _facility_definition_is_unsupported_vertical_proxy(definition: Variant) -> bool:
+ return block_vertical or super._facility_definition_is_unsupported_vertical_proxy(definition)
+
+func _initialize() -> void:
+ call_deferred("_run")
+
+func check(ok: bool, message: String) -> void:
+ if not ok:
+ failures += 1
+ push_error(message)
+
+func _run() -> void:
+ var runtime := GatedRuntime.new()
+ var clock := load("res://autoload/game_clock.gd").new()
+ var state := load("res://autoload/game_state.gd").new()
+ clock.reset(1, 6, 0)
+ state.reset()
+ check(runtime.initialize(clock, state, 90416)[&"valid"], "initialize")
+ runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
+ clock.set_paused(true)
+ var cell := Vector3i(-8, 2, 0)
+ var message := "先に階段の一つ下の階に通路を作ってください。"
+ var before := var_to_bytes(runtime.create_snapshot())
+ var report := runtime.preview_build_facility(&"guest_stairs", cell, 0)
+ check(not report[&"can_build"] and "STAIR_LANDING_MISSING" in report[&"error_codes"], "missing landing preview")
+ check(report[&"reasons"][0][&"message"] == message, "Runtime message")
+ check(before == var_to_bytes(runtime.create_snapshot()), "typed save snapshot byte identity")
+ var view := load("res://scenes/main/world_preview.gd").new()
+ view.set_runtime(runtime)
+ view.set_build_definition(runtime.definition_registry.get_facility(&"guest_stairs"))
+ check("STAIR_LANDING_MISSING" in view.get_build_preview_report(cell)[&"error_codes"], "WorldPreview surfaces Runtime")
+ var refused := runtime.submit_command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": cell})
+ check(refused.details.get(&"reason") == &"STAIR_LANDING_MISSING" and refused.message == message, "command agrees")
+ # Diagnostic path setup; existing reception supplies structural support.
+ var walkable: Array[Vector3i] = runtime.world_service.get_walkable_cells()
+ walkable.append(Vector3i(-8, 1, 0))
+ check(runtime.path_service.rebuild_graph(walkable), "diagnostic landing path")
+ report = runtime.preview_build_facility(&"guest_stairs", cell, 0)
+ var accepted := runtime.submit_command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": cell})
+ check(report[&"can_build"] and accepted.is_success(), "landing positive agreement")
+ runtime.block_operations = true
+ runtime.block_vertical = true
+ report = runtime.preview_build_facility(&"guest_stairs", Vector3i(-8, 4, 0), 0)
+ check(report[&"error_codes"][0] == "ARREARS_REMAIN", "operations precedence")
+ runtime.block_operations = false
+ report = runtime.preview_build_facility(&"guest_stairs", Vector3i(-8, 4, 0), 0)
+ check(report[&"error_codes"][0] == String(FacilityInfrastructureProjector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY), "vertical precedence")
+ view.free()
+ runtime.free()
+ clock.free()
+ state.free()
+ quit(1 if failures else 0)
diff --git a/tests/transport/test_runtime_build_preview.gd.uid b/tests/transport/test_runtime_build_preview.gd.uid
new file mode 100644
--- /dev/null
+++ b/tests/transport/test_runtime_build_preview.gd.uid
@@ -0,0 +1 @@
+uid://cpxbldpreview
+20 -10
View File
@@ -2781,17 +2781,17 @@ func _phase_name(phase: GameClock.Phase) -> String:
return "夜"
const RESOURCE_TITLES := {&"gold": "所持金", &"mana": "マナ", &"materials": "資材", &"faith": "信仰"}
func _resource_title(resource_name: StringName) -> String:
return String(RESOURCE_TITLES.get(resource_name, String(resource_name)))
func _on_resource_changed(resource_name: StringName, value: int) -> void:
if not resource_labels.has(resource_name):
return
var titles := {
&"gold": "所持金",
&"mana": "マナ",
&"materials": "資材",
&"faith": "信仰",
}
var label := resource_labels[resource_name] as Label
label.text = "%s %d" % [titles[resource_name], value]
label.text = "%s %d" % [_resource_title(resource_name), value]
if resource_name == &"mana":
label.mouse_filter = Control.MOUSE_FILTER_STOP
label.tooltip_text = "残量0で全館停止。接客・調理・搬入・エレベーターと新規入館が停止します。\n残量と配電が回復すると再開します。破壊・摘発による休止は解除されません。"
@@ -3169,12 +3169,22 @@ func _confirm_demolition() -> void:
# Paused play returns execution synchronously; running play returns acceptance.
if result.is_success() and _runtime.command_queue.get_processed_result(result.command_id) == null:
_pending_demolition_commands[result.command_id] = target
_set_status("撤去を受け付けました", true)
return
_show_demolition_result(result)
func _show_demolition_result(result: CommandResult) -> void:
var code := String(CommandResult.Code.keys()[result.code])
var reason := String(result.details.get(&"reason", ""))
_set_status("%s | %s | %s" % [code, reason, result.message], result.is_success())
if result.is_success():
var refund: Dictionary = result.details.get(&"refund", {})
var keys: Array = refund.keys()
keys.sort()
var amounts: PackedStringArray = []
for key: Variant in keys:
if int(refund[key]) != 0:
amounts.append("%s %d" % [_resource_title(StringName(key)), int(refund[key])])
_set_status("撤去しました(返金: %s)" % "、".join(amounts) if not amounts.is_empty() else "撤去しました(返金なし)", true)
else:
_set_status("撤去できません: " + result.message, false)
_refresh_selected_entity()
world_preview.queue_redraw()
+3 -3
View File
@@ -239,8 +239,8 @@ def make(base, source, checker):
row.update(expected=code, status=message)
elif label == 'missing_connection':
row['expected'] = 'STAIR_LANDING_MISSING'
row['results'] = [result('VALIDATION_FAILED', {'reason': row['expected']}, 'missing landing')]
row['status'] = '操作に失敗しました: missing landing'
report.update(can_build=False, error_codes=[row['expected']], reasons=[{'code':row['expected'], 'message':'先に階段の一つ下の階に通路を作ってください。', 'cells':[], 'details':{'reason':row['expected']}}])
row['status'] = report['reasons'][0]['message']
elif label == 'cancel_demolition':
row.update(kind='cancel', status='撤去を取り消しました。')
elif label in ('demolition', 'refused_demolition'):
@@ -257,7 +257,7 @@ def make(base, source, checker):
runtime['facilities']['instances'] = [v for v in runtime['facilities']['instances'] if v['instance_id'] != fid]
runtime['world']['occupancies'] = [v for v in runtime['world']['occupancies'] if v['entity_id'] != fid]
r = row['results'][0]
row['status'] = '%s | %s | %s' % (r['code'], r['details'].get('reason', ''), r['message'])
row['status'] = '撤去できません: '+r['message'] if row['expected'] else '撤去しました(返金: 資材 2)'
elif label == 'pan_zoom':
row.update(kind='camera', original_local=[1, 2], ctrl_pressed=False,
pan_before=[0,0], pan_after=[0,10],
+1 -1
View File
@@ -122,7 +122,7 @@ func build(id: StringName, cell: Vector3i, label: String, expected: String = "")
if expected.is_empty():
expect(not results.is_empty() and results.back().code == "OK", label + " accepted")
elif expected == "STAIR_LANDING_MISSING":
expect(not results.is_empty() and results.back().details.get("reason") == expected, label + " domain refusal")
expect(not report.can_build and expected in report.error_codes and results.is_empty(), label + " preview refusal")
else:
expect(expected in report.error_codes and results.is_empty(), label + " preview refusal")
var row := {"id": label, "kind": "build", "definition": String(id), "expected": expected,
+20 -2
View File
@@ -559,11 +559,20 @@ def action_gate(row, typed, root=HERE.parent):
expected_reason = REASONS[label][0] if label in REASONS else {'missing_connection':'STAIR_LANDING_MISSING', 'refused_demolition':'STAIR_LANDING_IN_USE'}.get(label, '')
require(row.get('expected', expected_reason) == expected_reason, 'required refusal reason changed')
results = row.get('results', [])
if kind == 'build' and results and results[0]['code'] != 'OK':
reason = results[0]['details'].get('reason', results[0]['code'])
require(not row['report']['can_build'], 'preview/command disagreement')
require(reason in row['report']['error_codes'], 'preview reason differs from Runtime reason')
if label in REASONS:
code, message = REASONS[label]
require(row['report']['error_codes'][0] == code and row['status'] == message, 'reason mismatch')
require(not row['report']['can_build'] and not results, 'preview rejection bypassed')
preserve(before, after, results)
elif label == 'missing_connection' and not results:
require(not row['report']['can_build'], 'preview/command disagreement')
require('STAIR_LANDING_MISSING' in row['report']['error_codes'], 'preview reason differs from Runtime reason')
require(row['report']['reasons'] and row['status'] == row['report']['reasons'][0]['message'], 'connection reason mismatch')
preserve(before, after, [])
elif kind == 'cancel':
require(not results and row['status'] == '撤去を取り消しました。', 'cancel submitted command')
preserve(before, after, results)
@@ -584,7 +593,16 @@ def action_gate(row, typed, root=HERE.parent):
else:
require(result['code'] == 'OK', 'positive rejected')
if kind == 'demolish':
require(row['status'] == '%s | %s | %s' % (result['code'], result['details'].get('reason', ''), result['message']), 'displayed domain reason mismatch')
tokens = (result['code'], result['details'].get('reason', ''), ' | ')
require(not any(token and token in row['status'] for token in tokens), 'raw demolition token in status')
if row['expected']:
require(row['status'] == '撤去できません: '+result['message'], 'demolition refusal message mismatch')
else:
titles = {'gold':'所持金', 'mana':'マナ', 'materials':'資材', 'faith':'信仰'}
refund = result['details']['refund']
amounts = ['%s %d' % (titles.get(k, k), int(refund[k])) for k in sorted(refund) if int(refund[k]) != 0]
expected_status = '撤去しました(返金: '+ '、'.join(amounts)+')' if amounts else '撤去しました(返金なし)'
require(row['status'] == expected_status, 'demolition refund display mismatch')
if not row['expected']:
fid = row['instance_id']
old = keyed(a['systems']['runtime']['construction']['facilities'], 'instance_id')
@@ -606,7 +624,7 @@ def action_gate(row, typed, root=HERE.parent):
require(row['report']['connection']['status'] == 'CONNECTED', 'positive connection absent')
if label == 'missing_connection':
require(row['status'] == '操作に失敗しました: '+result['message'], 'connection reason mismatch')
require(row['report']['can_build'], 'connection refusal did not reach Runtime')
require(not row['report']['can_build'], 'preview/command disagreement')
def check(project_root, evidence_dir):
root, evidence = Path(project_root).resolve(), Path(evidence_dir).resolve()
@@ -98,6 +98,83 @@ class EvidenceTests(unittest.TestCase):
def test_positive_whole_checker(self):
self.assertEqual([], C.check(self.root, self.out))
def action_row(self, label):
rows = C.load(self.out/'pair/writer-actions.json')['actions']
return copy.deepcopy(next(row for row in rows if row['id'] == label))
def action_check(self, row, message=None, mutate_state=None):
def typed(name):
node = C.load(self.out/('pair/'+name+'.json'))
if mutate_state and name == row['after']:
mutate_state(node)
return node
if message:
with self.assertRaisesRegex(ValueError, message):
C.action_gate(row, typed, self.root)
else:
C.action_gate(row, typed, self.root)
def test_agreeing_preview_refusal(self):
row = self.action_row('missing_connection')
self.assertEqual(row['status'], row['report']['reasons'][0]['message'])
self.action_check(row)
def test_preview_command_disagreement(self):
row = self.action_row('missing_connection')
row['report']['can_build'] = True
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
self.action_check(row, 'preview/command disagreement')
def test_preview_runtime_reason_difference(self):
row = self.action_row('missing_connection')
row['report']['error_codes'] = ['OVERLAP']
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
self.action_check(row, 'preview reason differs from Runtime reason')
def test_preview_partial_state_change(self):
self.action_check(self.action_row('missing_connection'), 'cancel/preview partially applied',
lambda node: C.typed_member(node, 'game_state', 'resources', 'materials').update(text='999'))
def test_readable_demolition_success(self):
row = self.action_row('demolition')
self.assertEqual('撤去しました(返金: 資材 2)', row['status'])
self.action_check(row)
def test_demolition_missing_refund_amount(self):
row = self.action_row('demolition'); row['status'] = '撤去しました(返金: 資材)'
self.action_check(row, 'demolition refund display mismatch')
def test_demolition_wrong_refund_amount(self):
row = self.action_row('demolition'); row['status'] = '撤去しました(返金: 資材 3)'
self.action_check(row, 'demolition refund display mismatch')
def test_demolition_raw_enum_status(self):
row = self.action_row('demolition'); row['status'] = 'OK | |'
self.action_check(row, 'raw demolition token in status')
def test_readable_demolition_refusal(self):
row = self.action_row('refused_demolition')
self.assertEqual('STAIR_LANDING_IN_USE', row['results'][0]['details']['reason'])
self.assertEqual('撤去できません: '+row['results'][0]['message'], row['status'])
self.action_check(row)
def test_demolition_raw_reason_status(self):
row = self.action_row('refused_demolition'); row['status'] += ' STAIR_LANDING_IN_USE'
self.action_check(row, 'raw demolition token in status')
def test_demolition_altered_refusal_message(self):
for status in ('撤去できません', '撤去できません: altered'):
row = self.action_row('refused_demolition'); row['status'] = status
self.action_check(row, 'demolition refusal message mismatch')
def test_demolition_raw_reason_mismatch(self):
row = self.action_row('refused_demolition')
row['results'][0]['details']['reason'] = 'OTHER'
def mutate(node):
items = C.typed_member(node, 'systems', 'runtime', 'command_queue', 'processed')['items']
items[-1] = F.typed(row['results'][0])
self.action_check(row, 'reason mismatch', mutate)
def test_verify_only_cli_uses_build_output_directory(self):
output = io.StringIO()
with patch.dict(os.environ, {'BUILD_OUTPUT_DIR':str(self.out.parent)}), contextlib.redirect_stdout(output):