From 8e3d79d5f2068fbe71c0ef7252540548d75b3d1e Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 14 Oct 2026 04:21:06 +0900 Subject: [PATCH] =?UTF-8?q?P08-03:=20=E9=85=8D=E7=BD=AE=E3=81=AE=E9=80=9A?= =?UTF-8?q?=E5=B8=B8=E6=93=8D=E4=BD=9C=E5=8F=97=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execution/p08-03/CONDITION_MATRIX.md | 49 +++++++- .../NATIVE_R02_READER_OPERATION_DELTA.json | 102 ++++++++++++++++ .../execution/p08-03/ROOT_COLLECTION.md | 15 +++ .../execution/p08-03/implementation.json | 53 +++++---- .../p08-03/local-source-manifest.json | 22 ++-- .../execution/p08-03/local-validation.json | 21 ++-- .../p08-03/native-r02-reader-stderr.log | 6 + .../p08-03/repair-r02-local-checker-tests.log | 5 + tests/fixtures/p08-03/checker_fixture.py | 41 ++++++- tests/fixtures/p08-03/placement.gd | 77 ++++++++++++- tools/check_p08_normal_placement_evidence.py | 97 +++++++++++++++- tools/test_p08_normal_placement_evidence.py | 109 ++++++++++++++++++ 12 files changed, 543 insertions(+), 54 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p08-03/repair-r02-local-checker-tests.log diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md b/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md index 7bc9fe2..e679579 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md @@ -11,7 +11,7 @@ This is one existing unit in 30 packages / 88 units, with no partial credit. | invalid_position_reason | Reception footprint crosses the boundary from an in-bounds origin; existing reception overlap. Exact displayed Japanese reason and report codes; whole typed before/after preservation. | Source and semantic corruption controls only; actual refusals pending. | | demolish_rebuild | Palette demolition toggle, cell selection, confirmation/cancel, Runtime `demolish_facility`, synchronous/queued results. Lower landing refusal, cancellation, reception removal, same-cell reconstruction. Occupancy, operational/construction instances, costs/refund checked. | Implemented; Godot parsing, actual UI, audio ownership, Runtime results pending. | | pan_zoom_world_identity | Move mode + non-Ctrl wheel; normal wheel zoom, 0.5/4.0 clamps; recomputed local/viewport/screen coordinates and actual selected cell. OS recorder requests CUA only. | Mock bridge/checker controls only; actual OS wheel/screenshot provenance pending. | -| separate_pid_resume | Formal four-member save; separate native writer/reader; normal load menu; whole typed/raw checkpoint identity, instance ID, cell and overlap judgement. | Collector/source prepared; real private profiles, actual distinct PIDs and formal saves pending. | +| separate_pid_resume | Formal four-member save; separate native writer/reader; normal load menu; whole typed/raw presave/formal identity and source-defined post-operation reconstruction, instance ID, cell and overlap judgement. | R02 diagnosed notification difference; repaired candidate requires fresh native/OS collection. | ## Original-matched diagnostic choices @@ -31,7 +31,7 @@ and a budget report explicitly tagged diagnostic. No production costs, services, balance values or definitions are changed. New art is unnecessary; the demolition button reuses Main's existing styling and audio. -## r01 fixture repair (current candidate) +## r01 fixture repair (retained earlier submission) The only failure authorities used are `NATIVE_R01_FAILURE_AND_SCOPE.json` and `native-r01-writer-stderr.log`. The r01 writer failed 13 expectations; @@ -51,12 +51,55 @@ explicitly recorded. Every observed Runtime save/load result and direct SaveService diagnostic return is recorded as typed JSON and SHA-pinned raw Variant, with PID, requested/absolute paths and user-data directory. -The 35 local checker tests remain synthetic controls. Their negative cases +The earlier 35 local checker tests remain synthetic controls. Their negative cases also cover missing backup members, slot metadata SHA, failed actual returns, and saves outside private profiles. The mock collector supplies a controlled disk-space input; the production disk-admission gate is unchanged. Current native/OS/full regression/review/integration remain pending. +## r02 whole-model operation repair (current candidate) + +The retained submitted HEAD is `c2b59f04f94dda0a61991ec3bb7f0e946196f995`. +`NATIVE_R02_READER_OPERATION_DELTA.json` and `native-r02-reader-stderr.log` +are supplied historical evidence: reader PID 58430 failed the old whole +postsave/postload equality assertion. Their observed 161/162 counts are +diagnostic observations, never constants in the checker or scenario. + +The fixture captures the complete writer model immediately before the normal +save, leaves `writer-checkpoint.var` as the complete postsave snapshot, and +uses `SaveService.inspect_snapshot` to decode the formal world read-only. +Both writer and reader retain inspection returned Dictionaries and complete +typed/raw formal models. The presave and formal models must match in full; +the reader formal model must match the writer formal model in full. + +For each role, the expected complete model starts from that formal model. +Only one `source_defined_notification` is constructed using Runtime lines +4680–4720, `StableIdGenerator.next_id`, `SimulationDomainEvent.to_dictionary` +and `DomainEventStream.publish/create_snapshot`. The payload is recomputed +from the actual returned Dictionary, including optional selected/manifest +fields and all timing fields. Event envelope, next ID, counter increment, +sorted published IDs, existing history and the source's recent-event limit +are derived independently. No fields or Runtime effects are removed, +normalized away or copied from the observed notification as an expectation. +The complete typed expectation and raw expected Variant must match the +complete postsave/postload checkpoint. Read-only inspection must also leave +the whole live model unchanged. + +Each role exports `*-expected-operation`, `*-operation-actual` and +`*-operation-comparison.json` alongside unchanged-format raw checkpoints, +formal models and returned-result records. On unknown drift, these complete +raw inputs remain and the checker reports the precise typed difference paths. +No new native difference was observed by this worker; fresh native execution +is pending, so the supplied R02 trace is not a PASS for these changed sources. + +The current Python suite retains all 35 earlier tests and adds 14 semantic +tests, including every notification payload/envelope field, type drift, +counter/published-ID errors, extra or rewritten history, unrelated/unknown +model drift, read-only formal mismatch and a non-trace-derived count with +the source's history-limit behavior. These are synthetic controls only. +Exact executed source/input SHAs and the current 49-test result are recorded +in `local-validation.json` and `implementation.json`. + ## Evidence boundary `placement.gd` contains the shared finite scenario and handler endpoints. diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json b/docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json new file mode 100644 index 0000000..d4e5bfb --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json @@ -0,0 +1,102 @@ +{ + "unit": "P08-03", + "submitted": "c2b59f04f94dda0a61991ec3bb7f0e946196f995", + "native_phases": { + "import": { + "pid": 58251, + "passed": true + }, + "parse": { + "pid": 58353, + "passed": true + }, + "writer": { + "pid": 58388, + "passed": true + }, + "reader": { + "pid": 58430, + "passed": false, + "failure": "whole formal resume model", + "failures": 1 + } + }, + "all_owned_ended": true, + "cleanup_errors": [], + "no_OS_started": true, + "no_full_regression": true, + "whole_typed_writer_reader_diff": [ + [ + "systems", + "runtime", + "domain_events", + "recent_events", + "161", + "event_type" + ], + [ + "systems", + "runtime", + "domain_events", + "recent_events", + "161", + "payload" + ] + ], + "writer_last_event": "SAVE_CHECKPOINT_SUCCEEDED", + "reader_last_event": "SAVE_RECOVERY_LOADED", + "formal_event_count": 161, + "post_operation_event_count": 162, + "formal_to_post_differences": { + "formal_to_post_operation_differences": { + "writer": [ + [ + "systems", + "runtime", + "id_generator", + "counters", + "domain_event" + ], + [ + "systems", + "runtime", + "domain_events", + "published_ids" + ], + [ + "systems", + "runtime", + "domain_events", + "recent_events" + ] + ], + "reader": [ + [ + "systems", + "runtime", + "id_generator", + "counters", + "domain_event" + ], + [ + "systems", + "runtime", + "domain_events", + "published_ids" + ], + [ + "systems", + "runtime", + "domain_events", + "recent_events" + ] + ] + } + }, + "existing_source_contract": "Runtime._complete_save_operation/_save_event_payload publishes the corresponding operation notification before save_operation_completed signal", + "diagnosis": "Whole post-save vs post-load raw-equality oracle contradicts distinct existing save/load notifications; all other typed fields match. Formal saved state must be compared fully, and exact operation effects must be verified without deleting/masking events. Not yet an acceptance or a product-defect conclusion.", + "required_fix_judgment": "Actual Opus must choose faithful saved-model + complete post-operation delta evidence; preserve all raw states/events/counters, no Runtime edits or criterion relaxation", + "registered_history": "old r01 preserved as historical-native-r01-7dd450c6; original published artifacts unchanged", + "acceptance_credit": 0, + "updated_utc": "2026-10-13T19:05:00.421862+00:00" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md b/docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md index b26e675..c04245b 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md @@ -1,5 +1,20 @@ # Root collection protocol (not executed by worker) +R02 repair requires a fresh exact-source collection. The supplied R02 reader +failure is historical evidence, not a PASS for the repaired candidate. +Keep every raw postsave/postload snapshot and returned Dictionary. In each +handler and OS pair the shared fixture now also emits writer-presave, +writer/reader-formal-model, expected-operation, operation-actual and +operation-comparison artifacts. Formal models come from the read-only normal +`SaveService.inspect_snapshot` API. `writer-checkpoint.var` remains the raw +postsave state; never replace it with the presave/formal model. +The checker reconstructs one source-defined notification from each actual +returned Dictionary and compares the complete expected typed/raw model. +Counts come from the saved model, not the historical 161/162 trace. Preserve +all artifacts on an unknown difference and record the checker difference +paths with the corresponding actual/expected files before another repair. +The collector and Runtime/SaveService/UI remain unchanged in this repair. + Read the fixed spec and condition matrix first. Freeze/commit through the authorized Root workflow. The worker has not committed or run this procedure. Use a clean checkout at the exact submitted HEAD, an installed native Godot, diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json b/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json index 5ec2b47..643f130 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json @@ -5,7 +5,7 @@ "P08-03" ], "base_commit": "12430ba89970bda349781e51157e72e48a4ac06f", - "actual_worktree_HEAD": "7dd450c64c897fc8e9e6bbd9fa242f28f42cb1af", + "actual_worktree_HEAD": "c2b59f04f94dda0a61991ec3bb7f0e946196f995", "worker_committed": false, "final_commit": null, "final_commit_note": "Root must freeze and record the submitted candidate; worker git writes are prohibited.", @@ -18,29 +18,31 @@ }, "final_local_files_sha256": { "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json": "f59f01f65c795057117fda753706a6200928d4954af901fc965cf1a7c09232b5", - "docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md": "3cbdc2bd8eccb8cf1f060fb237072dc5b935e8cd147c74884c8c1abb2c810c81", - "docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md": "ed1d3978bba78a9ffe68808ec1f1f1990ae180630b5eace9587b8dd1719b43ef", + "docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md": "e62103e73c9703f92c9361506aa7f7546a01c95b18eef25f827d249e33fb3a86", + "docs/plans/original-completion-2026-10-02/execution/p08-03/ROOT_COLLECTION.md": "79944e586fab86096de003fb0f6ad99b2af7f5d6c3043668edc0f9b39addc218", "docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd": "21855e3b32263eb98127202467b770cb1334fa70c6b357825a122db385ffa8d6", "docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd.uid": "d7f97560c8bbdfdaed173b2b48261e65b92847bd0493dd1cc2ebb309d0622314", "scenes/main/main.gd": "e015e18fee8c114ddce39e46a04be139e694a4e3569855065576c0c909b94fc6", "scenes/main/world_preview.gd": "2f0e417ae2ff1745062bdbdcec74b3b058cfcffbbd9001ca3e6a62cd23efb47d", - "tests/fixtures/p08-03/checker_fixture.py": "c6dd2471f408a790f174e5ab73bbdbd899838f51a0dcde91bd0655da63238c46", + "tests/fixtures/p08-03/checker_fixture.py": "0d12e09e5342a016bc9e61c056b07357e761c29f9d82fbf941ce524f51ff131e", "tests/fixtures/p08-03/passive_placement.gd": "c17a1874fa585ec1ea892b51860f0997176df084b08751ea41e27ea5bc1189e8", "tests/fixtures/p08-03/passive_placement.gd.uid": "f75e3f2d9555c30cee3a1de882f32225fc624565a78a7ef3f5c604325c578ce3", - "tests/fixtures/p08-03/placement.gd": "84296f340e26daff3e7cff50ae6e1d5ef8f717a39d9dfa9ff1b590059fa00a81", + "tests/fixtures/p08-03/placement.gd": "d47b54189be76b9c8e3a6f99c9ecdcbeb75b790cf4fb03d180be9654baa2548f", "tests/fixtures/p08-03/placement.gd.uid": "5caca08c1e627e0ca07e2ab7b13bd82b24a22f77513fe87e30b79023568e1987", "tests/ui/test_p08_normal_placement_ops.gd": "734f4f85d0b0fc45dc31fc23502425b911635667dabc0341ba0d88b0107f4405", "tests/ui/test_p08_normal_placement_ops.gd.uid": "7d168ac50ca82ae1979da31c467f9a5020ca5ec7616b5bf99294413d85b3b299", - "tools/check_p08_normal_placement_evidence.py": "9805c595b95172926f515e5600aaa14e19c83f0e9a2f88406b17656076b0e82c", + "tools/check_p08_normal_placement_evidence.py": "5baea283760e04f23abefcf576f9a3e8230b96ed30d82569528dcd211a6032cf", "tools/regression/suite-policy.json": "e875e6b05bbac8dfb98c2af007221436fa558ddb8806b6214689830295b32e8d", "tools/run_p08_normal_placement.py": "191831b4f684bdd7e99b3be6038c15a6830f544d17c941869b1c2a8fbe4a2aa3", - "tools/test_p08_normal_placement_evidence.py": "2c493349de1d72d1eae383356f8bf3904096c810550f9de373816a25d5ae54c8" + "tools/test_p08_normal_placement_evidence.py": "ebf6c0ef2c32dc8fac61d570a7fea99c1ac2499b92399db32afec26147b9d84c", + "docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json": "60463e735d4f473bdad7b0925c37b9ca71ee34d6f0eb74ea6c07b508b5153e41", + "docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log": "8079b3dc2ba7b9a9c37fcd9a5a8df0971354027c60def4b95fb15c7310832040" }, "executed_source_manifest": { "path": "local-source-manifest.json", - "sha256": "f3b5d4edb152c685bd10efa4ccbdb87fda5e3bdbb26a60c66bc12c1780a41003", + "sha256": "25b7492044046b5c8498f968b648392b592d42e266b2ff6fdb4773e52ba3e451", "source_file_count": 1326, - "contains": "Complete local execution source map and immutable/fixture input map; no material-size omission" + "contains": "Complete final local execution source and immutable/fixture/evidence input map; no material-size omission" }, "final_input_sha256": { "AGENTS.md": "8d8dc40211390275f858f322743309a0663c4f2d859780a8b8a08cea1450e8b3", @@ -50,21 +52,23 @@ "docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd.uid": "d7f97560c8bbdfdaed173b2b48261e65b92847bd0493dd1cc2ebb309d0622314", "docs/spec/acceptance/P08-03.json": "37a6a581b5a9bbe03c07a3a266b03df1f740ff488da33ecd9bd60f5b32f1efe1", "docs/traceability/original/BASELINE.txt": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a", - "tests/fixtures/p08-03/checker_fixture.py": "c6dd2471f408a790f174e5ab73bbdbd899838f51a0dcde91bd0655da63238c46", + "tests/fixtures/p08-03/checker_fixture.py": "0d12e09e5342a016bc9e61c056b07357e761c29f9d82fbf941ce524f51ff131e", "tests/fixtures/p08-03/passive_placement.gd": "c17a1874fa585ec1ea892b51860f0997176df084b08751ea41e27ea5bc1189e8", "tests/fixtures/p08-03/passive_placement.gd.uid": "f75e3f2d9555c30cee3a1de882f32225fc624565a78a7ef3f5c604325c578ce3", - "tests/fixtures/p08-03/placement.gd": "84296f340e26daff3e7cff50ae6e1d5ef8f717a39d9dfa9ff1b590059fa00a81", + "tests/fixtures/p08-03/placement.gd": "d47b54189be76b9c8e3a6f99c9ecdcbeb75b790cf4fb03d180be9654baa2548f", "tests/fixtures/p08-03/placement.gd.uid": "5caca08c1e627e0ca07e2ab7b13bd82b24a22f77513fe87e30b79023568e1987", + "docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json": "60463e735d4f473bdad7b0925c37b9ca71ee34d6f0eb74ea6c07b508b5153e41", + "docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log": "8079b3dc2ba7b9a9c37fcd9a5a8df0971354027c60def4b95fb15c7310832040", "docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R01_FAILURE_AND_SCOPE.json": "afc8dada6451e7e61d6d57abb0d7d040f5abcdb93432c8431c1c920dc4984515", "docs/plans/original-completion-2026-10-02/execution/p08-03/native-r01-writer-stderr.log": "07fe6cccad7305b2ec88cb91fa79fa22ec9269d7c99d62a1f69971f7c5d22686" }, "actual_local_validation": { "path": "local-validation.json", - "sha256": "531616ae3151b7a506978e43b754e1d6a372508515510da65c0fccffbbfff1c0", + "sha256": "ce90b9a8dc96b85736555df29d57ebddf3767df7c33043b2e9d39018c38e8729", "python_syntax": "pass: four final Python sources compiled in memory", - "checker_mock_tests": 35, + "checker_mock_tests": 49, "checker_mock_exit_code": 0, - "log_sha256": "0779ddd3305f3a2c6c719240e036bb1d7f0d10f97ce21854a0f946feefcace93", + "log_sha256": "0cb0e9deddcd9d90883a9416ecfed346bf1f4337a51247d08a39d49160313129", "git_diff_check": "pass" }, "conditions": { @@ -142,18 +146,23 @@ "P08_01_all30_claimed": false, "formal_acceptance": false, "repair": { - "prior_submitted_commit": "7dd450c64c897fc8e9e6bbd9fa242f28f42cb1af", + "prior_submitted_commit": "c2b59f04f94dda0a61991ec3bb7f0e946196f995", "failure_authorities": [ - "NATIVE_R01_FAILURE_AND_SCOPE.json", - "native-r01-writer-stderr.log" + "NATIVE_R02_READER_OPERATION_DELTA.json", + "native-r02-reader-stderr.log" ], "changes": [ - "fixture toggled signal and idempotent choose", - "private user:// formal and initial saves; SHA exports of members/backup/slot metadata", - "actual returned Dictionaries and paths captured prospectively" + "Complete presave/read-only formal/post-operation models retained", + "Source-defined single notification rebuilt independently from returned Dictionary with exact typed/raw complete-state comparison", + "Historical events and all other fields retained; unknown drift reports exact paths and complete raw artifacts" ], "production_changes": false, - "historical_r01_dicts": "absent; not reconstructed or appended", - "worker_git_writes": false + "collector_changes": false, + "worker_git_writes": false, + "trace_counts_hardcoded": false, + "new_unknown_native_differences_observed": false, + "native_repair_validation": "pending", + "context_gaps": [], + "historical_R02": "Writer passed and reader failed on c2b59f04; no acceptance or changed-source PASS inherited" } } diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/local-source-manifest.json b/docs/plans/original-completion-2026-10-02/execution/p08-03/local-source-manifest.json index b6b2412..211fdb9 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/local-source-manifest.json +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/local-source-manifest.json @@ -3678,8 +3678,8 @@ "sha256": "8f150654767de0c66329d7d9e84d306c4fa5a65c918e04fb520b626b39f68334" }, "tests/fixtures/p08-03/checker_fixture.py": { - "bytes": 22311, - "sha256": "c6dd2471f408a790f174e5ab73bbdbd899838f51a0dcde91bd0655da63238c46" + "bytes": 24737, + "sha256": "0d12e09e5342a016bc9e61c056b07357e761c29f9d82fbf941ce524f51ff131e" }, "tests/fixtures/p08-03/passive_placement.gd": { "bytes": 8799, @@ -3690,8 +3690,8 @@ "sha256": "f75e3f2d9555c30cee3a1de882f32225fc624565a78a7ef3f5c604325c578ce3" }, "tests/fixtures/p08-03/placement.gd": { - "bytes": 17684, - "sha256": "84296f340e26daff3e7cff50ae6e1d5ef8f717a39d9dfa9ff1b590059fa00a81" + "bytes": 21854, + "sha256": "d47b54189be76b9c8e3a6f99c9ecdcbeb75b790cf4fb03d180be9654baa2548f" }, "tests/fixtures/p08-03/placement.gd.uid": { "bytes": 20, @@ -5218,8 +5218,8 @@ "sha256": "928353233bf85436a5aaf8c4f77f3f82472d94b8ad2646cb1b030baf92b3b5dc" }, "tools/check_p08_normal_placement_evidence.py": { - "bytes": 34820, - "sha256": "9805c595b95172926f515e5600aaa14e19c83f0e9a2f88406b17656076b0e82c" + "bytes": 41464, + "sha256": "5baea283760e04f23abefcf576f9a3e8230b96ed30d82569528dcd211a6032cf" }, "tools/check_p24_selection_evidence.py": { "bytes": 14622, @@ -5298,8 +5298,8 @@ "sha256": "fd9ef7ec63f53dfd5e04e261606c9303d01a2e6590e22a35fef2e35df1429280" }, "tools/test_p08_normal_placement_evidence.py": { - "bytes": 13526, - "sha256": "2c493349de1d72d1eae383356f8bf3904096c810550f9de373816a25d5ae54c8" + "bytes": 20263, + "sha256": "ebf6c0ef2c32dc8fac61d570a7fea99c1ac2499b92399db32afec26147b9d84c" }, "tools/verify_exported_opening.py": { "bytes": 1519, @@ -5314,11 +5314,13 @@ "docs/plans/original-completion-2026-10-02/execution/p08-03/passive_os_placement_recorder.gd.uid": "d7f97560c8bbdfdaed173b2b48261e65b92847bd0493dd1cc2ebb309d0622314", "docs/spec/acceptance/P08-03.json": "37a6a581b5a9bbe03c07a3a266b03df1f740ff488da33ecd9bd60f5b32f1efe1", "docs/traceability/original/BASELINE.txt": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a", - "tests/fixtures/p08-03/checker_fixture.py": "c6dd2471f408a790f174e5ab73bbdbd899838f51a0dcde91bd0655da63238c46", + "tests/fixtures/p08-03/checker_fixture.py": "0d12e09e5342a016bc9e61c056b07357e761c29f9d82fbf941ce524f51ff131e", "tests/fixtures/p08-03/passive_placement.gd": "c17a1874fa585ec1ea892b51860f0997176df084b08751ea41e27ea5bc1189e8", "tests/fixtures/p08-03/passive_placement.gd.uid": "f75e3f2d9555c30cee3a1de882f32225fc624565a78a7ef3f5c604325c578ce3", - "tests/fixtures/p08-03/placement.gd": "84296f340e26daff3e7cff50ae6e1d5ef8f717a39d9dfa9ff1b590059fa00a81", + "tests/fixtures/p08-03/placement.gd": "d47b54189be76b9c8e3a6f99c9ecdcbeb75b790cf4fb03d180be9654baa2548f", "tests/fixtures/p08-03/placement.gd.uid": "5caca08c1e627e0ca07e2ab7b13bd82b24a22f77513fe87e30b79023568e1987", + "docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R02_READER_OPERATION_DELTA.json": "60463e735d4f473bdad7b0925c37b9ca71ee34d6f0eb74ea6c07b508b5153e41", + "docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log": "8079b3dc2ba7b9a9c37fcd9a5a8df0971354027c60def4b95fb15c7310832040", "docs/plans/original-completion-2026-10-02/execution/p08-03/NATIVE_R01_FAILURE_AND_SCOPE.json": "afc8dada6451e7e61d6d57abb0d7d040f5abcdb93432c8431c1c920dc4984515", "docs/plans/original-completion-2026-10-02/execution/p08-03/native-r01-writer-stderr.log": "07fe6cccad7305b2ec88cb91fa79fa22ec9269d7c99d62a1f69971f7c5d22686" } diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/local-validation.json b/docs/plans/original-completion-2026-10-02/execution/p08-03/local-validation.json index 98600c0..cb99ad2 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/local-validation.json +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/local-validation.json @@ -1,14 +1,14 @@ { "task_id": "P08-03", - "started_at": "2026-10-13T18:51:54.296622+00:00", - "ended_at": "2026-10-13T18:52:13.521432+00:00", + "started_at": "2026-10-13T19:19:51.362993+00:00", + "ended_at": "2026-10-13T19:20:39.117228+00:00", "syntax": { "method": "compile(source, filename, exec); no pyc", "files": { "tools/run_p08_normal_placement.py": "191831b4f684bdd7e99b3be6038c15a6830f544d17c941869b1c2a8fbe4a2aa3", - "tools/check_p08_normal_placement_evidence.py": "9805c595b95172926f515e5600aaa14e19c83f0e9a2f88406b17656076b0e82c", - "tools/test_p08_normal_placement_evidence.py": "2c493349de1d72d1eae383356f8bf3904096c810550f9de373816a25d5ae54c8", - "tests/fixtures/p08-03/checker_fixture.py": "c6dd2471f408a790f174e5ab73bbdbd899838f51a0dcde91bd0655da63238c46" + "tools/check_p08_normal_placement_evidence.py": "5baea283760e04f23abefcf576f9a3e8230b96ed30d82569528dcd211a6032cf", + "tools/test_p08_normal_placement_evidence.py": "ebf6c0ef2c32dc8fac61d570a7fea99c1ac2499b92399db32afec26147b9d84c", + "tests/fixtures/p08-03/checker_fixture.py": "0d12e09e5342a016bc9e61c056b07357e761c29f9d82fbf941ce524f51ff131e" }, "passed": true }, @@ -19,14 +19,17 @@ "tools/test_p08_normal_placement_evidence.py" ], "exit_code": 0, - "log": "repair-local-checker-tests.log", - "log_sha256": "0779ddd3305f3a2c6c719240e036bb1d7f0d10f97ce21854a0f946feefcace93", + "log": "repair-r02-local-checker-tests.log", + "log_sha256": "0cb0e9deddcd9d90883a9416ecfed346bf1f4337a51247d08a39d49160313129", "synthetic_only": true, - "test_count": 35 + "test_count": 49, + "retained_previous_tests": 35, + "new_semantic_tests": 14 }, "local_source_manifest": "local-source-manifest.json", - "local_source_manifest_sha256": "f3b5d4edb152c685bd10efa4ccbdb87fda5e3bdbb26a60c66bc12c1780a41003", + "local_source_manifest_sha256": "25b7492044046b5c8498f968b648392b592d42e266b2ff6fdb4773e52ba3e451", "source_and_inputs_unchanged_during_tests": true, + "git_diff_check": "pass", "native_engines_started": 0, "OS_operations": 0, "full_regression_executed": false, diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log b/docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log new file mode 100644 index 0000000..7b35d97 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/native-r02-reader-stderr.log @@ -0,0 +1,6 @@ +ERROR: P08 placement: whole formal resume model + at: push_error (core/variant/variant_utility.cpp:1023) + GDScript backtrace (most recent call first): + [0] expect (res://tests/fixtures/p08-03/placement.gd:36) + [1] reader (res://tests/fixtures/p08-03/placement.gd:249) + [2] frames (res://tests/fixtures/p08-03/placement.gd:68) diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/repair-r02-local-checker-tests.log b/docs/plans/original-completion-2026-10-02/execution/p08-03/repair-r02-local-checker-tests.log new file mode 100644 index 0000000..08c4aea --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/repair-r02-local-checker-tests.log @@ -0,0 +1,5 @@ +................................................. +---------------------------------------------------------------------- +Ran 49 tests in 44.792s + +OK diff --git a/tests/fixtures/p08-03/checker_fixture.py b/tests/fixtures/p08-03/checker_fixture.py index 7161127..d527734 100644 --- a/tests/fixtures/p08-03/checker_fixture.py +++ b/tests/fixtures/p08-03/checker_fixture.py @@ -117,10 +117,17 @@ def make(base, source, checker): folder.mkdir() runtime = {k: {} for k in sorted(checker.RUNTIME_KEYS)} runtime.update(world={'occupancies': [], 'bounds':{'min_x':-10,'max_y':10}}, construction={'facilities': [], 'jobs': []}, + id_generator={'version':1,'counters':{'domain_event':7}}, + domain_events={'version':1,'published_ids':[],'recent_events':[]}, facilities={'instances': []}, next_command_id=1, command_queue={'processed': [], 'pending': [], 'in_flight': [], 'accepted': [], 'idempotency': {}}) model = {'schema_version': 1, 'clock': {'absolute_minute': 360, 'paused': True}, 'game_state': {'resources': {'gold': 5000, 'materials': 50}}, 'systems': {'runtime': runtime}} + runtime['domain_events']['published_ids'] = ['domain_event.%012d' % n for n in range(1,8)] + runtime['domain_events']['recent_events'] = [ + {'event_id':'domain_event.%012d' % n, 'event_type':'SYNTHETIC_HISTORY', + 'absolute_minute':360, 'source_service':'synthetic', 'entity_ids':[], + 'payload':{'ordinal':n}, 'causation_command_id':0} for n in range(1,8)] target = [-8, 0, 0] cost = {'reception': {'gold': 500, 'materials': 8}, 'guest_stairs': {'gold': 40, 'materials': 0}, 'standard_room':{'gold':350,'materials':5}} rooms = 0 @@ -236,8 +243,10 @@ def make(base, source, checker): budget = {'setup': 'diagnostic', 'costs': [{'definition': d, 'cost': cost[d]} for d in ('reception', 'reception', 'guest_stairs', 'guest_stairs')], 'needed': {'gold': 1080, 'materials': 16}, 'initial': {'gold': 5000, 'materials': 50}, 'deltas': {}} put(folder/'writer-actions.json', {'setup': 'diagnostic', 'actions': actions, 'budget': budget}) - snap('writer-checkpoint') - snap('reader-checkpoint') + snap('writer-presave') + snap('writer-formal-model') + snap('reader-formal-model') + formal_model = typed(model) formal = folder/'formal' formal.mkdir() for member in checker.MEMBERS: (formal/member).write_text('synthetic '+member+'\n') @@ -251,14 +260,36 @@ def make(base, source, checker): 'members':{n:{'sha256':sha(folder/name/n),'bytes':(folder/name/n).stat().st_size} for n in checker.MEMBERS}} for name,suffix in [('formal',''),('formal.bak','.bak')]}, 'slot_metadata':{'file':'formal-slot-metadata.json','sha256':sha(formal/'manifest.json')}}) - for role,pid,op in [('writer',pids[0],'save'),('reader',pids[1],'load')]: + for role,pid,op in [('writer',pids[0],'save'),('reader',pids[1],'recovery')]: + returned = {'ok':True,'operation':op,'save_path':'user://saves/manual_ui', + 'code':'OK','message':'synthetic result','duration_us':13,'runtime_duration_us':17, + 'manifest':{'save_id':'synthetic-save','absolute_minute':360}} + if role == 'reader': + returned['selected'] = {'id':'formal','kind':'formal','generation':0} + expected = checker.expected_operation(formal_model,returned, + 'SAVE_CHECKPOINT_SUCCEEDED' if role == 'writer' else 'SAVE_RECOVERY_LOADED') + for label in ('checkpoint','expected-operation','operation-actual'): + put(folder/(role+'-'+label+'.json'),expected) + (folder/(role+'-'+label+'.var')).write_bytes(json.dumps(expected,sort_keys=True).encode()) + count = len(model['systems']['runtime']['domain_events']['recent_events']) + put(folder/(role+'-operation-comparison.json'), {'pid':pid,'delta_kind':'source_defined_notification', + 'formal_model':role+'-formal-model','actual_model':role+'-checkpoint', + 'expected_model':role+'-expected-operation','returned_result':typed(returned), + 'saved_recent_count':count,'expected_recent_count':count+1,'actual_recent_count':count+1, + 'whole_raw_equal':True}) stem = role+'-save-result-0001' (folder/(stem+'.var')).write_bytes(b'synthetic raw Dictionary stand-in') put(folder/(stem+'.json'), {'pid':pid,'operation':'runtime_operation', 'requested_path':'user://saves/manual_ui','absolute_path':absolute,'user_data_dir':profile, - 'result':typed({'ok':True,'operation':op,'save_path':'user://saves/manual_ui'}), + 'result':typed(returned), 'raw_sha256':sha(folder/(stem+'.var'))}) - put(folder/(role+'-save-results.json'), {'pid':pid,'records':[stem]}) + inspect_stem = role+'-save-result-0002' + (folder/(inspect_stem+'.var')).write_bytes(b'synthetic inspection Dictionary stand-in') + put(folder/(inspect_stem+'.json'), {'pid':pid,'operation':'formal_inspect', + 'requested_path':'user://saves/manual_ui','absolute_path':absolute,'user_data_dir':profile, + 'result':typed({'ok':True,'read_only':True,'snapshot':copy.deepcopy(model)}), + 'raw_sha256':sha(folder/(inspect_stem+'.var'))}) + put(folder/(role+'-save-results.json'), {'pid':pid,'records':[stem,inspect_stem]}) put(folder/'save.json', {'members': {n: sha(formal/n) for n in checker.MEMBERS}, 'pid': pids[0], 'cell': target, 'instance_id': 'f4'}) row = {'id': 'resume_overlap', 'kind': 'build', 'position': position(target), diff --git a/tests/fixtures/p08-03/placement.gd b/tests/fixtures/p08-03/placement.gd index a9ab948..3764383 100644 --- a/tests/fixtures/p08-03/placement.gd +++ b/tests/fixtures/p08-03/placement.gd @@ -20,6 +20,7 @@ var input_method := "handler" var observed_cell: Variant = null var save_serial := 0 var save_records: Array = [] +var operation_results: Array[Dictionary] = [] const INITIAL_SAVE := "user://saves/p08_initial" func annotate(row: Dictionary) -> void: @@ -43,8 +44,10 @@ func json_file(name: String, data: Variant) -> void: file.close() func capture(label: String) -> String: + return capture_model(label, runtime.create_snapshot()) + +func capture_model(label: String, snapshot: Dictionary) -> String: var name := role + "-" + label - var snapshot: Dictionary = runtime.create_snapshot() json_file(name + ".json", Codec.new().typed(snapshot)) var file := FileAccess.open(directory.path_join(name + ".var"), FileAccess.WRITE) if file != null: @@ -226,10 +229,15 @@ func writer() -> void: await demolish(target, "demolition") await build(&"reception", target, "rebuild") await camera() + capture("presave") await button(main.find_child("ManualSaveButton", true, false)) expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save") capture("checkpoint") await archive_formal() + var formal := inspect_formal() + var presave: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))) + expect(var_to_bytes(presave) == var_to_bytes(formal), "whole presave formal model") + verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED") var members := {} for member: String in MEMBERS: members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member)) @@ -245,8 +253,10 @@ func reader(distinct: bool) -> void: await frames() expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume") capture("checkpoint") - var original := FileAccess.get_file_as_bytes(directory.path_join("writer-checkpoint.var")) - expect(original == var_to_bytes(runtime.create_snapshot()), "whole formal resume model") + var formal := inspect_formal() + var original := FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) + expect(original == var_to_bytes(formal), "whole formal resume model") + verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED") expect(String(runtime.world_service.get_occupant(target)) == saved.instance_id, "same resumed instance/cell") for member: String in MEMBERS: expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "reader preserves formal member " + member) @@ -325,7 +335,68 @@ func save_initial_fixture() -> void: func archive_formal() -> void: export_package(String(main.manual_save_path), "formal") +func inspect_formal() -> Dictionary: + var stored := SaveService.inspect_snapshot(String(main.manual_save_path)) + record_save_result("formal_inspect", String(main.manual_save_path), stored) + expect(bool(stored.get(&"ok", false)), "read-only formal inspection") + var formal: Dictionary = stored.get(&"snapshot", {}) + capture_model("formal-model", formal) + return formal + +func verify_operation(formal: Dictionary, result: Dictionary, event_type: StringName) -> void: + var expected := formal.duplicate(true) + var model: Dictionary = expected[&"systems"][&"runtime"] + var counters: Dictionary = model[&"id_generator"]["counters"] + var next: int = int(counters.get("domain_event", 0)) + 1 + counters["domain_event"] = next + var event_id := "domain_event.%012d" % next + var payload := { + &"ok": bool(result.get(&"ok", false)), + &"code": StringName(result.get(&"code", &"UNKNOWN")), + &"message": str(result.get(&"message", "")), + &"operation": StringName(result.get(&"operation", &"unknown")), + &"save_path": str(result.get(&"save_path", result.get(&"path", ""))), + &"reason": StringName(result.get(&"reason", &"")), + &"request_id": int(result.get(&"request_id", 0)), + &"checkpoint_absolute_minute": int(result.get(&"checkpoint_absolute_minute", formal[&"clock"][&"absolute_minute"])), + &"duration_us": int(result.get(&"duration_us", 0)), + &"runtime_duration_us": int(result.get(&"runtime_duration_us", 0)), + } + if result.get(&"selected") is Dictionary: + var selected: Dictionary = result[&"selected"] + payload[&"selected_candidate_id"] = StringName(selected.get(&"id", &"")) + payload[&"selected_candidate_kind"] = StringName(selected.get(&"kind", &"")) + payload[&"selected_generation"] = int(selected.get(&"generation", -1)) + if result.get(&"manifest") is Dictionary: + var manifest: Dictionary = result[&"manifest"] + payload[&"save_id"] = str(manifest.get("save_id", "")) + payload[&"saved_absolute_minute"] = int(manifest.get("absolute_minute", -1)) + var stream: Dictionary = model[&"domain_events"] + stream["published_ids"].append(event_id) + stream["published_ids"].sort() + var notification := SimulationDomainEvent.new(StringName(event_id), event_type, + int(formal[&"clock"][&"absolute_minute"]), &"save_service", [], payload, 0) + stream["recent_events"].append(notification.to_dictionary()) + if stream["recent_events"].size() > DomainEventStream.RECENT_EVENT_LIMIT: + stream["recent_events"].pop_front() + capture_model("expected-operation", expected) + capture("operation-actual") + var actual: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join(role + "-checkpoint.var"))) + json_file(role + "-operation-comparison.json", { + "pid": OS.get_process_id(), "delta_kind": "source_defined_notification", + "formal_model": role + "-formal-model", "actual_model": role + "-checkpoint", + "expected_model": role + "-expected-operation", "returned_result": Codec.new().typed(result), + "saved_recent_count": formal[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(), + "expected_recent_count": stream["recent_events"].size(), + "actual_recent_count": actual[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(), + "whole_raw_equal": var_to_bytes(expected) == var_to_bytes(actual), + }) + # Complete raw actual/expected snapshots remain available on unknown drift. + expect(var_to_bytes(expected) == var_to_bytes(actual), "whole source-defined save operation model; see " + role + "-operation-comparison.json") + expect(var_to_bytes(actual) == var_to_bytes(runtime.create_snapshot()), "read-only inspection preserves live model") + func record_save_operation(result: Dictionary) -> void: + operation_results.append(result.duplicate(true)) record_save_result("runtime_operation", String(main.manual_save_path), result) func record_save_result(operation: String, path: String, result: Dictionary) -> void: diff --git a/tools/check_p08_normal_placement_evidence.py b/tools/check_p08_normal_placement_evidence.py index a53f963..e968c07 100644 --- a/tools/check_p08_normal_placement_evidence.py +++ b/tools/check_p08_normal_placement_evidence.py @@ -12,6 +12,7 @@ import sys import struct import zlib import subprocess +import copy sys.dont_write_bytecode = True HERE = Path(__file__).resolve().parent @@ -53,6 +54,68 @@ def load(path): return json.loads(Path(path).read_bytes()) def require(ok, message): if not ok: raise ValueError(message) +def scalar(value, kind=None): + kind = kind or ('bool' if isinstance(value, bool) else 'int' if isinstance(value, int) else 'String') + return {'type':kind, 'text':('&' if kind == 'StringName' else '')+json.dumps(value, ensure_ascii=False)} + +def dictionary(entries, key_type='String'): + return {'type':'Dictionary', 'entries':[[scalar(k, key_type), v] for k,v in entries.items()]} + +def typed_member(node, *path): + for key in path: + matches = [v for k,v in node['entries'] if decode(k) == key] + require(len(matches) == 1, 'missing/duplicate typed model member '+repr(path)) + node = matches[0] + return node + +def expected_operation(formal, result, event_type): + """Rebuild the full typed state from source rules, never from the actual delta. + + Runtime 4680-4720, StableIdGenerator.next_id, SimulationDomainEvent.to_dictionary + and DomainEventStream.publish/create_snapshot define this single notification. + """ + saved = decode(formal) + minute = saved['clock']['absolute_minute'] + values = {'ok':bool(result.get('ok',False)), 'code':str(result.get('code','UNKNOWN')), + 'message':str(result.get('message','')), 'operation':str(result.get('operation','unknown')), + 'save_path':str(result.get('save_path',result.get('path',''))), 'reason':str(result.get('reason','')), + 'request_id':int(result.get('request_id',0)), + 'checkpoint_absolute_minute':int(result.get('checkpoint_absolute_minute',minute)), + 'duration_us':int(result.get('duration_us',0)), 'runtime_duration_us':int(result.get('runtime_duration_us',0))} + names = {'code','operation','reason','selected_candidate_id','selected_candidate_kind'} + if isinstance(result.get('selected'),dict): + selected = result['selected'] + values.update(selected_candidate_id=str(selected.get('id','')), + selected_candidate_kind=str(selected.get('kind','')), selected_generation=int(selected.get('generation',-1))) + if isinstance(result.get('manifest'),dict): + manifest = result['manifest'] + values.update(save_id=str(manifest.get('save_id','')), saved_absolute_minute=int(manifest.get('absolute_minute',-1))) + expected = copy.deepcopy(formal) + runtime = typed_member(expected,'systems','runtime') + counters = typed_member(runtime,'id_generator','counters') + count = decode(counters).get('domain_event',0)+1 + require(0 < count <= 999999999999, 'invalid domain event counter') + for key,value in counters['entries']: + if decode(key) == 'domain_event': + value.update(scalar(count)); break + else: + counters['entries'].append([scalar('domain_event'),scalar(count)]) + event_id = 'domain_event.%012d' % count + stream = typed_member(runtime,'domain_events') + published = typed_member(stream,'published_ids') + require(event_id not in decode(published), 'duplicate operation notification') + published['items'].append(scalar(event_id)) + published['items'].sort(key=decode) + payload = dictionary({k:scalar(v,'StringName' if k in names else None) for k,v in values.items()},'StringName') + event = dictionary({'event_id':scalar(event_id), 'event_type':scalar(event_type), + 'absolute_minute':scalar(minute), 'source_service':scalar('save_service'), + 'entity_ids':{'type':'Array','element_type':4,'element_class':'','items':[]}, + 'payload':payload, 'causation_command_id':scalar(0)}) + recent = typed_member(stream,'recent_events') + recent['items'].append(event) + recent['items'] = recent['items'][-256:] + return expected + def png(data): require(data.startswith(b'\x89PNG\r\n\x1a\n'), 'missing OS screenshot') offset = 8; chunks = []; payload = b'' @@ -312,6 +375,8 @@ def check(project_root, evidence_dir): artifact(directory+'/'+name+'.var') return document(directory+'/'+name+'.json') rows = [] + normal_results = {} + inspections = {} for role, pid in zip(('writer', 'reader'), expected_pids): process = document(directory+'/'+role+'-process.json') require(process['pid'] == pid and process['failures'] == 0 and process['checks'] > 0, 'process attribution') @@ -329,6 +394,10 @@ def check(project_root, evidence_dir): require(isinstance(result, dict) and result.get('ok') is True, 'actual save/load failed') if record['operation'] == 'runtime_operation': operations.append(result.get('operation')) + normal_results[role] = result + if record['operation'] == 'formal_inspect': + require(result.get('read_only') is True and 'snapshot' in result, 'missing read-only formal model') + inspections[role] = typed_member(record['result'],'snapshot') require(('save' in operations) if role == 'writer' else any(op in ('load','recovery','recovery_candidate') for op in operations), 'missing actual normal save/load result') doc = document(directory+'/'+role+'-actions.json') require(doc['setup'] == 'diagnostic', 'unlabelled diagnostic') @@ -368,9 +437,33 @@ def check(project_root, evidence_dir): require(len(matches) == 1 and {tuple(c) for c in matches[0]['occupied_cells']} == cells, 'production footprint mismatch') rows += doc['actions'] checkpoint = typed('writer-checkpoint') - require(checkpoint == typed('reader-checkpoint'), 'wrong resume model') state(checkpoint) - require(sha(artifact(directory+'/writer-checkpoint.var')) == sha(artifact(directory+'/reader-checkpoint.var')), 'wrong raw resume') + formal = typed('writer-formal-model') + require(not typed_differences(typed('writer-presave'),formal), 'wrong presave formal model') + require(sha(artifact(directory+'/writer-presave.var')) == sha(artifact(directory+'/writer-formal-model.var')), 'wrong raw presave formal model') + require(not typed_differences(formal,typed('reader-formal-model')), 'wrong formal resume model') + require(sha(artifact(directory+'/writer-formal-model.var')) == sha(artifact(directory+'/reader-formal-model.var')), 'wrong raw formal resume model') + for role in ('writer','reader'): + require(role in inspections and not typed_differences(formal,inspections[role]), 'wrong read-only formal model') + result = normal_results[role] + require(result['operation'] == ('save' if role == 'writer' else 'recovery'), 'wrong operation notification kind') + event_type = 'SAVE_CHECKPOINT_SUCCEEDED' if role == 'writer' else 'SAVE_RECOVERY_LOADED' + expected = expected_operation(formal,result,event_type) + differences = typed_differences(expected,typed(role+'-checkpoint')) + require(not differences, 'wrong resume model: source-defined operation drift '+role+' '+repr(differences)) + require(not typed_differences(expected,typed(role+'-expected-operation')), 'wrong reconstructed operation model') + require(sha(artifact(directory+'/'+role+'-expected-operation.var')) == sha(artifact(directory+'/'+role+'-checkpoint.var')), 'wrong raw operation model') + require(not typed_differences(typed(role+'-checkpoint'),typed(role+'-operation-actual')), 'inspection changed live model') + require(sha(artifact(directory+'/'+role+'-operation-actual.var')) == sha(artifact(directory+'/'+role+'-checkpoint.var')), 'wrong raw inspection preservation') + comparison = document(directory+'/'+role+'-operation-comparison.json') + require(comparison == {'pid':expected_pids[0 if role == 'writer' else 1], + 'delta_kind':'source_defined_notification','formal_model':role+'-formal-model', + 'actual_model':role+'-checkpoint','expected_model':role+'-expected-operation', + 'returned_result':comparison['returned_result'], + 'saved_recent_count':len(decode(formal)['systems']['runtime']['domain_events']['recent_events']), + 'expected_recent_count':len(decode(expected)['systems']['runtime']['domain_events']['recent_events']), + 'actual_recent_count':len(decode(typed(role+'-checkpoint'))['systems']['runtime']['domain_events']['recent_events']), + 'whole_raw_equal':True} and decode(comparison['returned_result']) == result, 'wrong operation comparison attribution') saved = document(directory+'/save.json') require(set(saved['members']) == set(MEMBERS) and saved['pid'] == expected_pids[0], 'missing formal members/writer') for member in MEMBERS: diff --git a/tools/test_p08_normal_placement_evidence.py b/tools/test_p08_normal_placement_evidence.py index 16f83e9..8b62782 100644 --- a/tools/test_p08_normal_placement_evidence.py +++ b/tools/test_p08_normal_placement_evidence.py @@ -135,6 +135,115 @@ class EvidenceTests(unittest.TestCase): self.edit('pair/reader-save-result-0001.json', lambda d: d.update(result=F.typed({'ok':True,'operation':'load'}), requested_path='/outside/formal')) self.reject('save outside private user profile') + def test_notification_payload_is_recomputed_from_returned_dict(self): + path = 'pair/reader-checkpoint.json' + original = (self.out/path).read_bytes() + fields = ('ok','code','message','operation','save_path','reason','request_id', + 'checkpoint_absolute_minute','duration_us','runtime_duration_us', + 'selected_candidate_id','selected_candidate_kind','selected_generation', + 'save_id','saved_absolute_minute') + for field in fields: + with self.subTest(field=field): + data = json.loads(original) + event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1] + value = C.typed_member(event,'payload',field) + decoded = C.decode(value) + changed = not decoded if isinstance(decoded,bool) else decoded+1 if isinstance(decoded,int) else decoded+'-drift' + value.update(C.scalar(changed,value['type'])) + F.put(self.out/path,data); F.repin(self.out) + self.reject('source-defined operation drift') + (self.out/path).write_bytes(original); F.repin(self.out) + + def test_notification_envelope_cannot_drift(self): + path = 'pair/writer-checkpoint.json' + original = (self.out/path).read_bytes() + for field in ('event_id','event_type','source_service','absolute_minute','causation_command_id','entity_ids'): + with self.subTest(field=field): + data = json.loads(original) + event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1] + value = C.typed_member(event,field) + if field == 'entity_ids': value['items'].append(C.scalar('unexpected-entity')) + else: + old = C.decode(value) + value.update(C.scalar(old+1 if isinstance(old,int) else old+'-drift',value['type'])) + F.put(self.out/path,data); F.repin(self.out) + self.reject('source-defined operation drift') + (self.out/path).write_bytes(original); F.repin(self.out) + + def test_notification_counter_must_increment_exactly_once(self): + def corrupt(data): + C.typed_member(data,'systems','runtime','id_generator','counters','domain_event')['text'] = '9' + self.edit('pair/reader-checkpoint.json',corrupt) + self.reject('source-defined operation drift') + + def test_notification_published_ids_cannot_be_omitted(self): + self.edit('pair/writer-checkpoint.json',lambda d: C.typed_member(d,'systems','runtime','domain_events','published_ids')['items'].clear()) + self.reject('source-defined operation drift') + + def test_extra_notification_is_rejected(self): + def corrupt(data): + events = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'] + events.append(copy.deepcopy(events[-1])) + self.edit('pair/reader-checkpoint.json',corrupt) + self.reject('source-defined operation drift') + + def test_existing_event_history_cannot_be_rewritten(self): + def corrupt(data): + event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][0] + C.typed_member(event,'event_type').update(text='"REWRITTEN_HISTORY"') + self.edit('pair/reader-checkpoint.json',corrupt) + self.reject('source-defined operation drift') + + def test_postsave_unrelated_resource_drift_is_rejected(self): + self.edit('pair/writer-checkpoint.json',lambda d: C.typed_member(d,'game_state','resources','gold').update(text='9999')) + self.reject('source-defined operation drift') + + def test_postload_unknown_field_drift_is_rejected(self): + self.edit('pair/reader-checkpoint.json',lambda d: C.typed_member(d,'systems','runtime','inventory')['entries'].append([C.scalar('unknown'),C.scalar(1)])) + self.reject('source-defined operation drift') + + def test_presave_must_match_read_only_formal_model(self): + self.edit('pair/writer-presave.json',lambda d: C.typed_member(d,'clock','absolute_minute').update(text='361')) + self.reject('wrong presave formal model') + + def test_read_only_inspection_must_match_complete_formal_model(self): + self.edit('pair/reader-save-result-0002.json',lambda d: C.typed_member(d['result'],'snapshot','clock','absolute_minute').update(text='361')) + self.reject('wrong read-only formal model') + + def test_inspection_cannot_lose_read_only_attribution(self): + self.edit('pair/writer-save-result-0002.json',lambda d: C.typed_member(d['result'],'read_only').update(text='false')) + self.reject('missing read-only formal model') + + def test_returned_result_payload_tampering_is_rejected(self): + self.edit('pair/reader-save-result-0001.json',lambda d: C.typed_member(d['result'],'duration_us').update(text='999')) + self.reject('source-defined operation drift') + + def test_notification_payload_type_drift_is_rejected(self): + def corrupt(data): + event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1] + C.typed_member(event,'payload','operation')['type'] = 'String' + C.typed_member(event,'payload','operation')['text'] = '"recovery"' + self.edit('pair/reader-checkpoint.json',corrupt) + self.reject('source-defined operation drift') + + def test_operation_count_is_source_derived_with_recent_limit(self): + model = C.decode(json.loads((self.out/'pair/writer-formal-model.json').read_text())) + runtime = model['systems']['runtime'] + runtime['id_generator']['counters']['domain_event'] = 900 + stream = runtime['domain_events'] + stream['published_ids'] = ['domain_event.%012d' % n for n in range(1,901)] + stream['recent_events'] = [{'ordinal':n} for n in range(645,901)] + expected = C.decode(C.expected_operation(F.typed(model),{'ok':True},'SAVE_CHECKPOINT_SUCCEEDED')) + new = expected['systems']['runtime'] + self.assertEqual(901,new['id_generator']['counters']['domain_event']) + self.assertEqual(901,len(new['domain_events']['published_ids'])) + self.assertEqual(256,len(new['domain_events']['recent_events'])) + self.assertEqual(stream['recent_events'][1:],new['domain_events']['recent_events'][:-1]) + self.assertEqual('domain_event.000000000901',new['domain_events']['recent_events'][-1]['event_id']) + del runtime['id_generator']['counters'] + with self.assertRaisesRegex(ValueError,'missing/duplicate typed model member'): + C.expected_operation(F.typed(model),{'ok':True},'SAVE_CHECKPOINT_SUCCEEDED') + def test_missing_formal_member(self): for name in ('formal','formal.bak'): with self.subTest(package=name):