From 78f7cba7a9fb5e5dcb144758e1a86fe63b3d7d08 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Sat, 17 Oct 2026 14:49:39 +0900 Subject: [PATCH] =?UTF-8?q?P24-02:=20=E6=93=8D=E4=BD=9C=E3=81=A8=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E3=81=AE=E5=85=A8=E5=8F=97=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 2 +- .../repair4/check_repair4_preservation.py | 15 +- .../repair4/test_checker_roster_head.py | 8 +- ...ENT_MAIN_RECONCILIATION_APPLICABILITY.json | 26 + .../REFERENCE_MANIFEST.json | 40 + ...OT_EXECUTION_AND_BINDING_CLARIFICATIONS.md | 11 + .../CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json | 99 + .../actual-limited-review/result.json | 29 + ...23_life_observation_evidence.py.source.txt | 2503 +++++++++++++++++ .../tools/run_all_tests.py.source.txt | 311 ++ .../run_p23_life_observation.py.source.txt | 362 +++ .../LOCAL_COMMAND_RESULTS.json | 92 + .../source-ready-repair5-20261017/PROTOCOL.md | 31 + .../REFERENCE_INTEGRITY.json | 30 + .../SOURCE_MANIFEST.json | 134 + .../source-ready-repair5-20261017/STATUS.json | 80 + .../composition.py | 262 ++ .../correspondence.py | 198 ++ .../diff_check.log | 2 + .../source-ready-repair5-20261017/import.log | 3 + .../missing_real_evidence.log | 4 + .../preservation.log | 37 + .../py_compile.log | 2 + .../repair4_tests.log | 16 + .../repair5_tests.log | 28 + .../test_repair5.py | 328 +++ scenes/main/main.gd | 1 + tests/ui/test_operations_notification.gd | 25 + tools/check_p24_operations_evidence.py | 86 +- tools/regression/suite-policy.json | 2 +- 30 files changed, 4729 insertions(+), 38 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/CURRENT_MAIN_RECONCILIATION_APPLICABILITY.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/REFERENCE_MANIFEST.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/ROOT_EXECUTION_AND_BINDING_CLARIFICATIONS.md create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/check_p23_life_observation_evidence.py.source.txt create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_all_tests.py.source.txt create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_p23_life_observation.py.source.txt create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/LOCAL_COMMAND_RESULTS.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/PROTOCOL.md create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/REFERENCE_INTEGRITY.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/SOURCE_MANIFEST.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/STATUS.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/correspondence.py create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/diff_check.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/import.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/missing_real_evidence.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/preservation.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/py_compile.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair4_tests.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair5_tests.log create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py diff --git a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json index 68b8dc8..932e8c4 100644 --- a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json +++ b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json @@ -3740,7 +3740,7 @@ "template": "Operations notification: checks, failures", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "9c26386c529b8c8135639e8d0f0ed390da7139d64ff1e268975b05948cab9147", + "source_sha256_at_schema_audit": "50172ab6daa031d221f6a78cac5cdbcb8884f295f8062b54656562e2412faff2", "authority": { "path": "docs/spec/acceptance/P24-02-code-r01.json", "scope": "new local suite source marker; actual execution pending" diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py b/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py index 403c72f..a2259ee 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py @@ -305,6 +305,18 @@ def check_json(path: str, main_text: str, report: dict) -> None: ((base_t, "base"), (main_text, "main"), (p24_t, "p24"))) recon = load_json_strict(recon_t, f"{path}@reconciled") expected = expect3(base, main, p24) + # Repair5 authority permits exactly this P24 source pin rebind; no schema, + # cleanup policy, non-P24 row or ordered structure is changed. + test = "tests/ui/test_operations_notification.gd" + digest = hashlib.sha256((ROOT / test).read_bytes()).hexdigest() + if path.endswith("completion_marker_contracts.json"): + rows = [r for r in expected["rows"] if r["test"] == test] + if len(rows) != 1: + fail("exactly one P24 marker row required") + else: + rows[0]["source_sha256_at_schema_audit"] = digest + elif path.endswith("suite-policy.json"): + expected["suite_policies"][test]["source_sha256"] = digest ok = ordered_equal(recon, expected) if not ok: fail(f"{path}: reconciled JSON != main snapshot + P24-only additions (value/order)") @@ -459,7 +471,8 @@ def main() -> int: report["scope"] = ("Static source preservation only. Not Godot/import/native/OS/full-roster proof and " "not acceptance. Root owns current-main composition and all execution.") report["failures"] = failures - EVIDENCE.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + if args.write_evidence: + EVIDENCE.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") summary = {p: {k: (len(v) if isinstance(v, list) else v) for k, v in r.items() if k in ("missing", "duplicates", "unexpected", "required_count", "reconciled_symbol_count", "equals_main_plus_p24_only_additions_in_order", "body_issues")} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py b/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py index 4beab0f..f1a07ec 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py @@ -33,13 +33,11 @@ class CheckerTests(unittest.TestCase): def validate(self, rows, **counts): data = dict(test_suite='tests/ui/test_operations_notification.gd', failures=0, checks=1, full_roster=dict(results=rows, **counts)) - previous = Path.cwd() - try: - os.chdir(self.root) + # Repair5 deliberately resolves suite bytes from the verified T view, + # never from cwd. This TEST-ONLY adapter supplies that isolated view. + with patch.object(checker, 'VERIFIED_ROOT', self.root): return checker._validate_condition_artifact_content( 'final_validation_review', data, 'json', self.root, {}, {}) - finally: - os.chdir(previous) def test_positive_roster(self): for counts in ({}, dict(total_suites=3, suites_count=3, failures=0)): diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/CURRENT_MAIN_RECONCILIATION_APPLICABILITY.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/CURRENT_MAIN_RECONCILIATION_APPLICABILITY.json new file mode 100644 index 0000000..406cc65 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/CURRENT_MAIN_RECONCILIATION_APPLICABILITY.json @@ -0,0 +1,26 @@ +{ + "actual_main": "54cade4181f6c3c4f5701ba54ac5fcbeca695945", + "exact4_main_blobs": [ + { + "path": "scenes/main/main.gd", + "actual_main_blob": "fff37f082d750bb91c71f2aa54d168c9e2cb8fe7", + "equals_already_reconciled_reference_blob": true + }, + { + "path": "scenes/main/world_preview.gd", + "actual_main_blob": "fa32fe0344d20565b6d542da1256a94e8dee1845", + "equals_already_reconciled_reference_blob": true + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json", + "actual_main_blob": "2eecb803f5eb6774ff38cd353de1f02847aeb0f9", + "equals_already_reconciled_reference_blob": true + }, + { + "path": "tools/regression/suite-policy.json", + "actual_main_blob": "ad194ae425fb8123fa191ed3635f404c098f692d", + "equals_already_reconciled_reference_blob": true + } + ], + "reason": "M54 is docs-only after original M51 reference; all four conflict-input blobs are genuinely identical. Future M changing any conflict-input blob needs new reconciliation proof, not automatic old-S overwrite." +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/REFERENCE_MANIFEST.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/REFERENCE_MANIFEST.json new file mode 100644 index 0000000..0ad21bd --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/REFERENCE_MANIFEST.json @@ -0,0 +1,40 @@ +{ + "source": "bd3a13405115fc9bf6c5acec55bcfef7dd15266a", + "main": "54cade4181f6c3c4f5701ba54ac5fcbeca695945", + "all_files_complete": true, + "file_or_byte_cap": null, + "reference_only_not_executable_snapshot_imports": true, + "files": [ + { + "path": "tools/check_p23_life_observation_evidence.py", + "reference_path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/check_p23_life_observation_evidence.py.source.txt", + "sha256": "e41479ae51bccdfab3f31bd348794b441c2f341179bbe33222e2aacdf88f987f", + "git_blob": "8a62d1e1070688ced445e48d49b831fc7e9cfea3" + }, + { + "path": "tools/run_p23_life_observation.py", + "reference_path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_p23_life_observation.py.source.txt", + "sha256": "8dbd90e22f47912b3381b514398fe1fb2c403c97d8e3d0dd675e9c8db5b5d0d1", + "git_blob": "0a255140d48e3cf194c1644948a0f1e1dc89a3ca" + }, + { + "path": "tools/run_all_tests.py", + "reference_path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_all_tests.py.source.txt", + "sha256": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4", + "git_blob": "502ad8c1f6ba371446ecfd8f37f0f9194cdb10ea" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json", + "reference_path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json", + "sha256": "ef17de03039bcd2b3d7a2851d26c0abdc163b32fef40048d933a693f1b49114f", + "git_blob": "fec14229d322f4cbe9e27f2e02eb08f0e89bef31" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json", + "reference_path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json", + "sha256": "9c7892ce6ccd26d997e7a609be29ef8a12acb2fb7df75cf4def6f11a8a9157dc", + "git_blob": "7a35430785c15b510dfe107c03807bf5c75951ba" + } + ], + "instruction": "P23 helper code is reference only; preserve exact generic Git/source provenance where applicable, do not copy P23 marker/acceptance identities as P24 evidence or import missing modules into old-base S." +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/ROOT_EXECUTION_AND_BINDING_CLARIFICATIONS.md b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/ROOT_EXECUTION_AND_BINDING_CLARIFICATIONS.md new file mode 100644 index 0000000..ed4a218 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/ROOT_EXECUTION_AND_BINDING_CLARIFICATIONS.md @@ -0,0 +1,11 @@ +# Root operational facts for repair5 + +Actual source bd3a134, actual Main54cade4. The4conflict main blobs equal the original M51 snapshots, verified by Git and recorded in CURRENT_MAIN_RECONCILIATION_APPLICABILITY.json. Bind this reconciliation input provenance: a later M changing any of these4blobs must not silently reuse an old-S resolution and drop new Main code. Docs-only M updates may be applicable by exact relevant blob identity, with fresh genuine Q/M roles; do not relabel oldQ. Additional negative: newQ with currentM as parent but stale conflict S blobs dropping changed M hunk must fail against the reconciled-input binding. + +Spec/source/marker/settings/fixture pins are Git file blobs. The engine executable is external and cannot have a Git blob ID; use a separately verified external engine fingerprint/actual started+ended runtime receipt binding and explicit same engine across actual inputs. Do not invent a path or require an external binary to be a repository blob. Source-only tests may use labelled TEST-ONLY data; no real index or acceptance is created. Actual Root capture will supply real engine path/SHA/version and OS platform. Existing P23 receipts are format references only, never P24 actual execution. + +If adding a native UI test changes tests/ui/test_operations_notification.gd source, update only its own marker/policy source-authority SHA fields to the actual new test source after verifying unchanged marker schema/cleanup semantics. Keep all non-P24 entries and all existing safety rules. Do not leave a stale source pin, and do not lower marker expectations. The original registered scope already allows the two JSON files. Adjust local preservation/input checks for this explicit new-source SHA rebind; never claim unchanged old SHA. + +Do not edit NEXT_CLOSURE.md or other Main progress/shared Root reference files; Main54 changed them and doing so adds unrelated merge conflicts. New status/docs/helpers/tests belong under source-ready-repair5-20261017 or another specifically directed p24-02 owned subdir. Keep renderer world_preview.gd unchanged unless Opus explicitly instructs a necessary fix. The new modal visibility callback is functional correctness, not a visual redesign. + +Root bridge owns prelaunch HEAD/no competing worker/quota checks and exclusive lease. A restricted worker must not ps or search user folders. Task-local TMPPREFIX and PYTHONPYCACHEPREFIX are ordinary environment routing within already writable allocation; both solved real default temp/cache failures in the identical sandbox. No policy, permission, security or billing changes. Root can set these before the actual model process so every shell receives correct defaults. Native/Godot/OS/full executions remain Root-owned and not worker actions. diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json new file mode 100644 index 0000000..7a35430 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json @@ -0,0 +1,99 @@ +{ + "plan_id": "P24-02", + "unit_state": "in_progress", + "runner_state": "submitted", + "submitted_sha": "bd3a13405115fc9bf6c5acec55bcfef7dd15266a", + "source_backup_remote_branch": "codex/p24-02-source-bd3a134", + "same_existing_task": "the-tree-p24-02-operations/P24-02", + "repair_count": 4, + "max_repair_attempts": null, + "actual_roles": "Opus5.5 commander; existing Antigravity IDE Opus5.5Medium GUI partial4file merge, then verified terminal quota; Opus-authorized Codex6.1-sol same-task nonUI continuation, no duplicateworker/registration/counterreset", + "static_readiness": { + "actual_compile_pass": true, + "actual_preservation_pass": true, + "actual_diff_check_pass": true, + "meaningful_local_tests": 9, + "local_tests_reused_only_with_3_input_hashes_and_actual_raw_exit0": true, + "actual_source_and_JSON_move_guards_pass": true + }, + "limited_independent_review": { + "actual_Opus_run": "95cffd7b552a4939a4252349b3dfcfbd", + "status": "pass", + "critical_high_medium": 0, + "low": 5, + "original_unit_acceptance_ids": [], + "scope": "Static source readiness only; not Core whole-unit review evidence." + }, + "prepared_real_composition": { + "scope": "Prospective real Git composed source, not a normal integration candidate or runtime evidence. Actual failed merge and stage1/2/3 are preserved; Root resolved only4 known conflicts by taking exact already-reconciled S blobs.", + "submitted_sha": "bd3a13405115fc9bf6c5acec55bcfef7dd15266a", + "main_context_sha": "51e8657bfbe24856186cc6530358c5459cf909b6", + "prepared_tree": "a0667b90d13bd3ae8d8b8bdbc580192c2f90622f", + "prepared_commit": "4bf9a1b80e1491d138954d218720b4bf6298d32a", + "prepared_worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-root-composed-bd3a134-r01", + "actual_parents": [ + "51e8657bfbe24856186cc6530358c5459cf909b6", + "bd3a13405115fc9bf6c5acec55bcfef7dd15266a" + ], + "actual_conflict_resolution_bindings": [ + { + "path": "scenes/main/main.gd", + "resolved_actual_sha256": "f71f8c15493d6b51002ee4afb41cc349efe77e860944c1f79a5b24f928ecbd24", + "actual_S_blob": "a3bba86dd156437ce6b802bcb49597cb94a45312", + "authority": "Exact submitted S content with actual static body/symbol/order preservation proofs; Root resolved index only, no authored source edits" + }, + { + "path": "scenes/main/world_preview.gd", + "resolved_actual_sha256": "8c2daa9e95d56e6e40bb2071b206693069cb7e4731b4d660d59d41c34cb43dfa", + "actual_S_blob": "26dd8d785f37f6c9142c80b7deaf8daecb0cebe6", + "authority": "Exact submitted S content with actual static body/symbol/order preservation proofs; Root resolved index only, no authored source edits" + }, + { + "path": "tools/regression/suite-policy.json", + "resolved_actual_sha256": "adc2fbac2463bfdb047bede0182c66083996b75d2bd542a3b8640672d7ca2400", + "actual_S_blob": "de0ab07c83c144a6c469cfdcf52ad8bd6c0bd6fa", + "authority": "Exact submitted S content with actual static body/symbol/order preservation proofs; Root resolved index only, no authored source edits" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json", + "resolved_actual_sha256": "1063e578eefe6da1b38c24f4c004669bcac67a21884636fb1b9c7e96e4ecf388", + "actual_S_blob": "68b8dc8324b7928164bddbcbb1dbb49fbeb25f89", + "authority": "Exact submitted S content with actual static body/symbol/order preservation proofs; Root resolved index only, no authored source edits" + } + ], + "selected_source_files": 1368, + "required_roster_size_NOT_EXECUTED": 180, + "all_git_blob_bindings": 4835, + "source_maps": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/.runtime/coordinator-resume-20261017-r01/prepared-bd3a134-source-maps", + "historical_EOF_lint": { + "exit_code": 2, + "output": "docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_CODE_PHASE_PRELAUNCH_PROOF/original-GUI-copy/scenes/main/main.gd.source.txt:3319: new blank line at EOF.\nscenes/main/main.gd:3319: new blank line at EOF.\nscripts/ui/manual_save_slots_dialog.gd:219: new blank line at EOF.\n", + "disposition": "Extra blank EOF lines inherited in actual source and exact preservation copy; retained byte-identically. Recorded source-style observations only, not an invented runtime/spec requirement and not claimed clean lint. Local repair4 actual delta git-diff-check passed. Not product behavior or a conflict marker." + }, + "native_runs": 0, + "full_runs": 0, + "all8_original_P24_accepted": false, + "normal_integration_candidate": false, + "production_main_unchanged": true, + "original_completed_units": 14, + "at_jst": "2026-10-17T14:10:32.615822+09:00" + }, + "first_next_required_batch": [ + "Implement strict same-root actual Git S/M/T/Q composition/source/input/roster binding, including deterministic4conflict resolution, using existing P23/shared helpers without relabeling tests.", + "Replace artifact-ID count proxy with actual condition/provenance/content correspondence; one all-keys JSON plus irrelevant fillers must fail; genuine shared raw traces may be reused only with concrete scope bindings.", + "Cover actual staff-panel click equivalent (direct handler call is not OS mapclick), defense modal visibility guard, actual bubble render/state transitions; investigate heart semantics/multiple-link order and per-frame relation scans without inventing gameplay thresholds.", + "Treat preservation body-line set weakness and always-written evidence flag as recorded low hardening; do not claim stronger automatic proof than provided and preserve independent manual inspection scope.", + "Batch required related fixes then actual import/local/impact, actual normalOS/scale/save evidence, exact final full once, whole8 formal independent review, normalintegration/final/ledger; Windows-specific conditions remain genuine actual-device requirements." + ], + "all8_original_conditions_accepted": false, + "new_native_runs": 0, + "new_full_roster_runs": 0, + "original_denominator": { + "packages": 30, + "units": 88, + "completed": 14, + "partial_credit": false + }, + "no_source_code_adoption_on_main": true, + "at_jst": "2026-10-17T14:21:51.543429+09:00" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json new file mode 100644 index 0000000..fec1422 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json @@ -0,0 +1,29 @@ +{ + "task_id": "P24-02", + "commit": "bd3a13405115fc9bf6c5acec55bcfef7dd15266a", + "status": "pass", + "summary": "Limited independent static review of the Q checkout (S=bd3a134 blobs composed onto M=51e8657). I used Read/Grep/Glob only and ran nothing. This is static source-readiness only: none of the 8 original P24 conditions is accepted, no acceptance_ids are added, and nothing here counts as Godot, native, OS, full-roster, Windows or whole-unit review evidence.\n\n(1) Reconciled files. world_preview.gd keeps the P23 LifeOverlay pipeline: _draw_contents/_build_life_observation, the overlay pass/serial report, the actor_drawings rows, aggregation and hidden filtering, and the cabin passengers. The single new adapter, _draw_actor_bubble(row, canvas), only reads state. It skips riding rows, looks up live Staff/Guest through get_staff/get_guest and RelationshipState through get_relationships, and never writes the observation or its rows. _draw_unit_bubble's fitted-row origin at+(-1p,-15p) works out to exactly P24's old center+(-9p, h/2-3wp-32p), so geometry, glyphs and colours are unchanged; only the target canvas is different. The 10 'reexpressed' _draw_actors lines are covered by main's _observe_actors/_draw_actors; staff queue placement survives at world_preview.gd:1433. The P24 additions in main.gd (modal guard, domain-event stream bind/unbind, cut-in, camera controls, click handler) match the GUI-copy bytes. Both JSON files are main's entries plus the P24 row/key appended at the end. No new production values were found.\n\n(2) check_p24_operations_evidence.py. The real HEAD now comes from git, and CANDIDATE_COMMIT/TARGET_HEAD are cross-checks only; git failure or a mismatch gives an empty result. The required suite set is loaded from the shared run_all_tests.roster. Suite sets must match exactly. Unknown or duplicate rows and non-dict rows are rejected, and every row needs a valid SHA that matches the file on disk, failures as an int equal to 0 (bool rejected), and passed=True. Optional counts must match exactly when present. The raw diff shows both old count-bypass `pass` branches removed, and the OS, receipt, PID, condition and Windows guards are untouched. The 9 stdlib tests include real positive controls (roster with and without counts, HEAD in a throwaway git repo) and meaningful negatives (invented replacement, duplicate padding, unknown extra rows, failures/passed type variants, SHA variants, malformed rows and counts, env mismatch, git unavailable).\n\n(3) check_repair4_preservation.py. It compares scope-qualified symbols across base, P24, main and the reconciled file, plus body lines per symbol, flags P24 edits to symbols main removed, and does a strict 3-way JSON comparison by value and order with duplicate-key detection. The METRIC_IDS regex fix correctly reads the assigned array past the Array[StringName] annotation. The bubble schema checks are pinned to M.\n\n(4) Docs. SOURCE_READY/NOTRUN status for all 8 conditions, the earlier failed py_compile and JSON-order runs, the GUI quota stop and the stale 179 run are all recorded honestly. The Root S/M/T/Q composition contract is still listed as an unresolved prerequisite, not as passed.\n\nI found no critical, high or medium defects. The low findings below are hardening items for the later required phases, which all remain mandatory: Root composition contract, import/impact, normal OS, exact final full roster, whole-8 independent review, normal integration and Windows.", + "acceptance_ids": [], + "findings": [ + { + "severity": "low", + "message": "scenes/main/main.gd:985-987 connects world_preview.staff_assignment_clicked to assign_staff_by_click, but nothing in world_preview.gd emits that signal (it is only declared at world_preview.gd:6; _gui_input never emits it). This was already the case in P24 (no P24 _gui_input lines were lost) and is not a repair4 regression. A non-drag alternative already exists through the staff panel's facility_option and apply_button, so this is dead wiring rather than a missing feature. However, tests/ui/test_operations_notification.gd:110 calls assign_staff_by_click directly, so click_alternatives evidence must not treat that direct call as a real map-click path. Either emit the signal from the facility-click branch (respecting modal_input_blocked) or remove the connection." + }, + { + "severity": "low", + "message": "main.gd:3259 _update_modal_input_guard includes defense_manager in its dialog list, but _ensure_defense_manager (main.gd:3063-3089) never connects defense_manager.visibility_changed to the guard, unlike the other six dialogs. Opening or closing the defense dialog alone does not update world_preview.modal_input_blocked. AcceptDialog's exclusive default probably masks this, but modal_and_pause_resume runtime evidence should cover it or the connection should be added." + }, + { + "severity": "low", + "message": "check_repair4_preservation.py compares function bodies as sets of stripped lines (lines 176-203), so it cannot detect reordered statements or a dropped copy of a repeated line (for example a duplicate return/queue_redraw). REPAIR4_REEXPRESSED_P24_LINES also allows every lost P24 line in the two listed functions, not just specific lines. I checked the 19 enumerated lines by hand and they are legitimately re-expressed, but the guard is weaker than an ordered/multiset body check. Also, --write-evidence is parsed but has no effect: evidence is always written. This is documented in the status file." + }, + { + "severity": "low", + "message": "check_p24_operations_evidence.py: get_actual_git_head() resolves the repo from the checker's own location (parents[1]), while the roster, source_hashes and suite SHA checks use the current working directory (Path('.')). If it is run from another checkout, HEAD and source evidence come from different trees. The existing 'distinct artifacts' guard (lines 716-722) only counts IDs: one artifact containing every condition's keys plus 7 hash-valid filler artifacts would pass, because each condition needs only one artifact to validate. Neither problem is new to repair4 or a weakened guard, but both should be closed in the Root S/M/T/Q composition contract before the real index verify-only run." + }, + { + "severity": "low", + "message": "Bubble coverage and semantics (already marked NOTRUN, recorded so they are not lost): (a) no local test exercises the new _draw_actor_bubble/_draw_unit_bubble render path; tests only call OperationFeedback.resolve_*_bubble. (b) _staff_relationship/_guest_relationship return the first RelationshipState involving the actor, so with several relationships the heart bubble depends on iteration order. (c) The inherited heart rule 'shield>0 or affection>0 or route-lock pending/locked' needs whole-8 review against 'no invented thresholds'. (d) Each frame scans every relationship for every drawn actor." + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/check_p23_life_observation_evidence.py.source.txt b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/check_p23_life_observation_evidence.py.source.txt new file mode 100644 index 0000000..8a62d1e --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/check_p23_life_observation_evidence.py.source.txt @@ -0,0 +1,2503 @@ +#!/usr/bin/env python3 +"""Partial read-only semantic diagnostics. This version cannot accept P23-03. + +Whole typed inputs are preserved. The existing typed codec is reused verbatim. +Unimplemented visual/OS checks are explicit blockers, never successful checks. +""" +import argparse +import contextlib +import tempfile +import copy +import hashlib +import importlib.util +import json +import math +import os +from pathlib import Path +import re +import sys +import struct +import subprocess +import zlib + +sys.dont_write_bytecode = True +ROOT = Path(__file__).resolve().parent.parent +SPEC = 'docs/spec/acceptance/P23-03.json' +SPEC_SHA = '8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f' +CONDITIONS = ( + 'room_occupancy_and_operation', 'queues_and_overflow', + 'elevator_state_and_passengers', 'staff_state_and_jobs', + 'supplies_and_carried_goods', 'same_demand_route_improvement', + 'normal_os_and_distinct_pid_resume', +) +CATEGORIES = ('room', 'queue', 'EV', 'staff', 'goods', 'business') +CONDITION_CHECKS = dict(zip(CONDITIONS, ('room_users', 'queue_overflow', 'elevator_passengers', + 'staff_jobs', 'carried_goods', 'route_improvement', 'distinct_pid_resume'))) +# Required names, not verdicts. BLOCKERS is computed from actual execution below. +REQUIRED_CHECKS = {condition: {category: {'semantic_' + category, 'pixels_' + category, + CONDITION_CHECKS[condition], 'os_guards', 'resume_recalculation'} + for category in CATEGORIES} for condition in CONDITIONS} + + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +codec = module('p23_existing_typed_codec', ROOT / 'tools/check_p08_migration_evidence.py') +def decode(node): + # Keep P08 scalar semantics, adding the renderer's Rect2 and vector keys. + if node['type'] == 'Dictionary': + result = {} + for key, value in node['entries']: + key = decode(key) + if isinstance(key, list): + key = tuple(key) + require(key not in result, 'duplicate typed dictionary key') + result[key] = decode(value) + return result + if node['type'] == 'Array': + return [decode(value) for value in node['items']] + if node['type'] == 'Rect2': + match = re.fullmatch(r'Rect2\(([^()]*)\)', node['text']) + require(match is not None, 'invalid typed Rect2') + values = [float(v.strip()) for v in match[1].split(',')] + require(len(values) == 4 and all(math.isfinite(v) for v in values), 'invalid rectangle') + return values + return codec.decode(node) + + +def require(value, message): + if not value: + raise ValueError(message) + + +def check_setup_budget_source(source): + """Cheap preflight for the fixture's shared spend plan, before OS replay.""" + prepare = source.split('func prepare() -> void:', 1)[1].split('\nfunc ', 1)[0] + categories = { + 'construction': ('budget_builds := builds.duplicate(true)', + 'budget_builds.append([improvement[', + 'for row: Array in budget_builds:', + 'needed[key] += int(definition.construction_cost.get(key, 0))'), + 'signing': ('for assignment: Array in assignments:', + 'runtime.mvp_content_catalog.get_recruitment_entry(assignment[0])', + 'signing_total += signing_cost', 'needed[&"gold"] += signing_total'), + 'purchase': ('SupplyCatalog.get_item(SupplyCatalog.RAW_FOOD)', + 'PurchaseQuoteV2.make(SupplyCatalog.RAW_FOOD, purchase_quantity,', + 'PurchaseQuoteV2.eligible(runtime)', 'purchase_quote[&"effective_total"]', + 'needed[&"gold"] += purchase_total'), + } + budget_end = prepare.index('\n\tvar state:') + for category, expressions in categories.items(): + require(all(expression in prepare[:budget_end] for expression in expressions), + 'setup budget missing spend category: ' + category) + require(prepare.count('for assignment: Array in assignments:') == 2, + 'budget and hire must share assignments') + require('&"quantity": purchase_quantity}' in prepare, + 'budget and purchase must share quantity') + require('deltas[key] = needed[key] - state.get_resource(key)' in prepare, + 'setup budget deltas must cover needed') + return set(categories) + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def png_pixels(data): + """Decode noninterlaced 8-bit RGB/RGBA PNG, validating the whole stream.""" + require(data[:8] == b'\x89PNG\r\n\x1a\n', 'PNG signature') + pos, header, payload, ended, idat_closed = 8, None, bytearray(), False, False + while pos < len(data): + require(pos + 12 <= len(data), 'PNG truncated chunk') + size, kind = struct.unpack('>I4s', data[pos:pos + 8]) + require(pos + size + 12 <= len(data), 'PNG truncated payload') + body = data[pos + 8:pos + 8 + size] + crc = struct.unpack('>I', data[pos + 8 + size:pos + 12 + size])[0] + require(zlib.crc32(kind + body) == crc, 'PNG CRC mismatch') + pos += size + 12 + require(header is not None or kind == b'IHDR', 'PNG IHDR must be first') + if kind == b'IHDR': + require(header is None and size == 13, 'PNG duplicate/invalid IHDR') + w, h, depth, color, compression, filtering, interlace = struct.unpack('>IIBBBBB', body) + require(w > 0 and h > 0 and depth == 8 and color in (2, 6) + and (compression, filtering, interlace) == (0, 0, 0), 'PNG unsupported IHDR') + header = (w, h, 3 if color == 2 else 4) + elif kind == b'IDAT': + require(not idat_closed, 'PNG nonconsecutive IDAT') + payload.extend(body) + elif kind == b'IEND': + require(size == 0 and payload and pos == len(data), 'PNG invalid IEND') + ended = True + break + else: + require(kind[0] & 32 or kind == b'PLTE', 'PNG unknown critical chunk') + if payload: + idat_closed = True + require(ended and header is not None, 'PNG missing IEND') + w, h, channels = header + stride = w * channels + expected = h * (stride + 1) + try: + decoder = zlib.decompressobj() + raw = decoder.decompress(bytes(payload), expected + 1) + require(decoder.eof and not decoder.unused_data and not decoder.unconsumed_tail + and len(raw) == expected, 'PNG zlib length/end mismatch') + except zlib.error as exc: + raise ValueError('PNG invalid zlib') from exc + rows, prior = [], bytearray(stride) + for y in range(h): + start = y * (stride + 1) + mode = raw[start] + require(mode in range(5), 'PNG unknown filter') + row = bytearray(raw[start + 1:start + 1 + stride]) + for x in range(stride): + left = row[x - channels] if x >= channels else 0 + up = prior[x] + corner = prior[x - channels] if x >= channels else 0 + if mode == 1: + predictor = left + elif mode == 2: + predictor = up + elif mode == 3: + predictor = (left + up) // 2 + elif mode == 4: + p = left + up - corner + distances = (abs(p - left), abs(p - up), abs(p - corner)) + predictor = (left, up, corner)[distances.index(min(distances))] + else: + predictor = 0 + row[x] = (row[x] + predictor) & 255 + rows.append([tuple(row[x:x + channels]) for x in range(0, stride, channels)]) + prior = row + return w, h, rows + + +def image_kind(data): + if data.startswith(b'\x89PNG\r\n\x1a\n'): + w, h, _ = png_pixels(data) + return 'png', w, h + if data.startswith(b'\xff\xd8'): + helper = module('p23_p08_jpeg', ROOT / 'tools/check_p08_normal_placement_evidence.py') + w, h = helper.jpeg_dimensions(data) + return 'jpeg', w, h + raise ValueError('unknown screenshot byte magic') + + +def inspect_conversion(rec, cua_receipt, converter_sha256=None, resolve_path=Path): + """CUA receipt and converter pin are independent of conversion assertions.""" + raw, png = resolve_path(rec['raw_path']), resolve_path(rec['png_path']) + require(raw.resolve() != png.resolve() and not raw.is_symlink() and not png.is_symlink(), + 'conversion must preserve a separate raw file') + require(rec['raw_sha256'] == sha(raw) == cua_receipt['raw_sha256'] + and rec['tool_call_id'] == cua_receipt['tool_call_id'] + and bool(rec['tool_call_id']), 'CUA raw hash/call mismatch') + require(rec['raw_kind'] == 'jpeg', 'conversion raw kind') + kind, w, h = image_kind(raw.read_bytes()) + require(kind == 'jpeg', 'conversion requires JPEG magic') + require(rec['converter'] == '/usr/bin/sips', 'conversion executable') + pin = converter_sha256 if converter_sha256 is not None else sha('/usr/bin/sips') + require(rec['converter_sha256'] == pin, 'converter SHA mismatch') + require(rec['argv'] == ['/usr/bin/sips', '-s', 'format', 'png', rec['raw_path'], '--out', rec['png_path']], + 'converter argv mismatch') + require(type(rec['exit_code']) is int and rec['exit_code'] == 0, 'converter exit code') + for key in ('stdout_sha256', 'stderr_sha256'): + require(isinstance(rec[key], str) and re.fullmatch('[0-9a-f]{64}', rec[key]), + 'invalid converter output digest') + require(sha(png) == rec['png_sha256'], 'converted PNG SHA mismatch') + pw, ph, rows = png_pixels(png.read_bytes()) + require((w, h) == (pw, ph) == (rec['width'], rec['height']), 'conversion dimensions mismatch') + return pw, ph, rows + + +def screenshot_pixels(path, conversion=None, cua_receipt=None, converter_sha256=None): + data = Path(path).read_bytes() + kind, _, _ = image_kind(data) + if kind == 'jpeg': + require(conversion is not None, 'JPEG screenshot requires conversion record') + require(Path(conversion['raw_path']).resolve() == Path(path).resolve(), 'conversion raw path mismatch') + require(cua_receipt is not None, 'missing CUA receipt') + return inspect_conversion(conversion, cua_receipt, converter_sha256) + return png_pixels(data) + + +def affine_point(matrix, point): + require(len(matrix) == 6 and all(type(v) in (int, float) and math.isfinite(v) for v in matrix), 'invalid affine') + a, b, c, d, x, y = matrix + require(a*d-b*c != 0, 'singular affine') + return [a*point[0]+c*point[1]+x, b*point[0]+d*point[1]+y] + + +def affine_rect(matrix, rect): + x, y, w, h = rect + points = [affine_point(matrix, p) for p in ([x,y], [x+w,y], [x,y+h], [x+w,y+h])] + lo = [min(p[i] for p in points) for i in (0, 1)] + return lo + [max(p[i] for p in points)-lo[i] for i in (0, 1)] + + +def screenshot_transform(calibration, request, image_size, clicks): + """Content -> window screenshot pixels, cross-checked against actual clicks. + + This is independent of the renderer's content-space screen_rect. In + particular, window_position is a desktop coordinate, not an image origin. + No titlebar height, scale, or image extent is assumed by the checker. + """ + scale = calibration['backing_scale'] + stretch = calibration['canvas_stretch'] + titlebar = calibration['titlebar_pixels'] + require(all(type(v) in (int, float) and math.isfinite(v) for v in + (scale, stretch, titlebar)) and scale > 0 and stretch > 0 and titlebar >= 0, + 'invalid screenshot calibration') + close(calibration['window_size'], request['window_size'], 'calibrated window size') + close(calibration['window_position'], request['window_position'], 'calibrated window position') + close(calibration['screenshot_pixels'], list(image_size), 'calibrated image size') + close([request['screen_scale'], request['content_scale_factor']], [scale, stretch], + 'calibrated engine scale') + close([request['window_size'][0]*scale, request['window_size'][1]*scale+titlebar], + list(image_size), 'calibrated content extent') + matrix = [scale, 0, 0, scale, 0, titlebar] + matched = 0 + for receipt in clicks: + if receipt.get('target_pid') != request['target_pid']: + continue + for event in receipt['events']: + if event.get('action') != 'click': + continue + close(event['calibration'], calibration, 'click calibration') + point = event['point'] + close(affine_point(point['viewport_to_screen'], point['viewport']), + point['screen'], 'click content affine') + close(affine_point(matrix, point['screen']), event['coordinates'], + 'click screenshot offset') + matched += 1 + require(matched > 0, 'missing independent click calibration') + return matrix + + +def rect_intersection(a, b): + require(len(a) == len(b) == 4 and all(type(v) in (int, float) and math.isfinite(v) + for v in a+b) and min(a[2:]+b[2:]) >= 0, 'invalid clipping rectangle') + x, y = max(a[0], b[0]), max(a[1], b[1]) + return [x, y, max(0, min(a[0]+a[2], b[0]+b[2])-x), + max(0, min(a[1]+a[3], b[1]+b[3])-y)] + + +def rectangle_eligibility(rect, viewport): + clipped = rect_intersection(list(rect), [0, 0, *viewport]) + if not clipped[2] or not clipped[3]: + return 'outside', clipped + return ('visible' if clipped == list(rect) else 'clipped'), clipped + + +def paired_crop_noise(before, after, rect): + """Measured temporal differences, never an invented JPEG RGB tolerance. + + A stable pair alone does not establish class or state identity. The result + is diagnostic data only and must not be promoted to pixels_. + """ + require(before[:2] == after[:2], 'paired image dimensions') + x, y, w, h = rect_intersection(list(rect), [0, 0, *before[:2]]) + errors = [max(abs(a-b) for a, b in zip(before[2][py][px][:3], after[2][py][px][:3])) + for py in range(math.ceil(y), math.floor(y+h)) + for px in range(math.ceil(x), math.floor(x+w))] + require(errors, 'empty noise crop') + return dict(samples=len(errors), max_channel_delta=max(errors), + mean_channel_delta=sum(errors)/len(errors), unchanged=sum(v == 0 for v in errors)) + + +def drawing_rectangle_rows(report): + """Derive identities and local positions; do not read audit verdicts.""" + if any('anchor' in b for key in ('badges', 'landing_badges', 'actor_aggregations') + for b in report.get(key, [])): + validate_observation_labels(report) + expected = [] + tower = 'scripts/ui/pixel_tower_art.gd' + people = 'scripts/ui/pixel_people.gd' + world = 'scenes/main/world_preview.gd' + def actor(a, category=None): + expected.append((category or ('staff' if a['staff'] else 'guest'), a['key'], + a['at'] + [16*a['scale'], 18*a['scale']], people, None)) + def badge(category, identifier, b): + expected.append((category, identifier, badge_rectangle(b['at'], b['text']), world, b['text'])) + for fid, room in report['rooms'].items(): + if 'rect' in room: + expected.append(('room', fid, room['rect'], tower, None)) + for i, b in enumerate(report['landing_badges']): + badge('queue', str(i), b) + for cabin in report['cabins']: + expected.append(('EV', cabin['shaft_id'], cabin['rect'], tower, None)) + for a in cabin['passengers']: + actor(a) + for a in report['actor_drawings']: + actor(a) + if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']: + actor(a, 'rest') + for i, b in enumerate(report.get('actor_aggregations', [])): + badge('aggregation', str(i), b) + for i, b in enumerate(report['badges']): + badge('goods' if b['kind'] == 'supply' else 'business', str(i), b) + for row in report.get('traffic', []): + if row['traffic']['current_density'] > 0 or row['traffic']['temporary_block']: + expected.append(('traffic', ','.join(map(str, row['cell'])), row['rect'], world, None)) + return expected + + +PIXEL_PEOPLE_CORE_SHA256 = 'a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c' + + +def actor_identity_core(actor): + """Grid identity bounds from the pinned PixelPeople pose, including bob. + + Standard-room breathing extends the upper edge, not the lower edge. + Arms, legs, props and the sleep symbol are outside the identity core. + """ + key, at, scale = actor['key'], actor['at'], actor['scale'] + require(len(at) == 2 and all(type(v) in (int, float) and math.isfinite(v) for v in at) + and type(scale) in (int, float) and math.isfinite(scale) and scale > 0, + 'invalid actor footprint: ' + key) + require(isinstance(actor.get('mode'), str) and type(actor.get('frame')) is int + and type(actor.get('flip')) is bool, 'missing actor pose: ' + key) + mode, phase = actor['mode'], actor['frame'] % 8 + if mode in ('sleep', 'collapsed', 'standard_room_sleep'): + breathing = -1 if mode == 'standard_room_sleep' and phase in (2, 3, 4) else 0 + x, y, w, h = 2, 11 + breathing, 13, 5 - breathing + else: + bob = -1 if mode in ('walk', 'carry_walk') and phase in (2, 6) else 0 + x, y, w, h = 6, 1 + bob, 6, 12 + if actor['flip']: + x = 16 - x - w + return [at[0] + x * scale, at[1] + y * scale, w * scale, h * scale] + + +def validate_actor_nonoverlap(report, root=ROOT, transit_keys=()): + """Recompute identity cores from drawing inputs, including cabin passengers. + + Do not consume audit eligibility or boolean judgments: neither an off-screen + label nor a claimed pass exempts a submitted actor from this invariant. + Transparent margins may overlap. No asserted transit flag grants exemption. + """ + require(hashlib.sha256((root / 'scripts/ui/pixel_people.gd').read_bytes()).hexdigest() + == PIXEL_PEOPLE_CORE_SHA256, 'PixelPeople core source SHA256') + actors = list(report['actor_drawings']) + [ + actor for cabin in report['cabins'] for actor in cabin['passengers']] + rects = [] + keys = set() + for actor in actors: + key = actor['key'] + require(key not in keys, 'duplicate actor drawing: ' + key) + keys.add(key) + rect = actor_identity_core(actor) + if key in transit_keys: + continue + for other_key, other in rects: + intersects = (rect[0] < other[0] + other[2] and other[0] < rect[0] + rect[2] + and rect[1] < other[1] + other[3] and other[1] < rect[1] + rect[3]) + require(not intersects, 'actor-actor core overlap: ' + other_key + ' / ' + key) + rects.append((key, rect)) + + +def validate_drawing_rows(report, audit, root=ROOT, state=None, rest_audit=None): + # Without raw inputs this is deliberately conservative: NO transit exemption. + # With inputs, use the same state-derived classification and fit proof as the + # stream checker, never a drawing's region/in_transit assertion. + if state is None: + validate_actor_nonoverlap(report, root) + else: + require(isinstance(state, dict) and isinstance(state.get('systems', {}).get('runtime'), dict), + 'malformed raw state context') + if report['resting_staff']: + require(rest_audit is not None, 'missing rest authority for drawing geometry') + if rest_audit is not None: + require(isinstance(rest_audit, dict) and isinstance(rest_audit.get('staff'), list), + 'malformed rest authority') + compare_rest(state, report, rest_audit, root) + compare_cabin_geometry(state, report) + compare_actor_geometry(state, report) + expected = drawing_rectangle_rows(report) + require(len(audit['rows']) == len(expected), 'offline rectangle coverage') + for row, (category, identifier, rect, source, text) in zip(audit['rows'], expected): + require((row['category'], row['id'], row['style_source']) == (category, identifier, source), + 'offline rectangle identity') + rect = list(rect) + if text is not None: + require(row['text'] == text and type(row['font_width']) in (int, float) + and math.isfinite(row['font_width']) and row['font_width'] > 0, 'offline font/text') + require(row['font_width'] <= rect[2] - 6, 'badge text exceeds reproducible rectangle') + close(row['rect'], rect, 'offline report drawing rectangle') + colors = list(dict.fromkeys(re.findall(r'Color\("([0-9a-fA-F]{6})"', (root/source).read_text()))) + require(row['style_colors'] == colors and colors, 'offline source palette provenance') + + +def encode_png(rows): + """Lossless review crop from decoded pixels, without an imaging dependency.""" + require(rows and rows[0], 'empty crop') + width, channels = len(rows[0]), len(rows[0][0]) + require(channels in (3, 4), 'crop channels') + def chunk(kind, body): + return (struct.pack('>I', len(body)) + kind + body + + struct.pack('>I', zlib.crc32(kind + body))) + raw = b''.join(b'\0' + bytes(v for pixel in row for v in pixel) for row in rows) + return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack( + '>IIBBBBB', width, len(rows), 8, 2 if channels == 3 else 6, 0, 0, 0)) + + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')) + + +def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT, state=None, rest_audit=None): + """One mechanical path for frozen diagnostics and actual OS frames. + + Errors are retained as failures, including incomplete frozen audits. Every + supplied row is accounted for; no palette value is an image oracle. + """ + errors, rows = [], [] + transit_authority = ('raw state, track, time and pause' if state is not None + else 'absent: fail closed, no transit exemptions') + try: + validate_drawing_rows(report, audit, root, state, rest_audit) + except (ValueError, KeyError, TypeError) as exc: + errors.append(str(exc)) + calibration = screenshot_transform(receipt['calibration'], request, images[0][:2], clicks) + for image in images: + require(image[:2] == images[0][:2], 'frame image dimensions') + for point in ([0, 0], [1, 0], [0, 1]): + close(affine_point(request['viewport_to_screen'], affine_point(request['local_to_viewport'], point)), + affine_point(request['local_to_screen'], point), 'frame affine composition') + for index, row in enumerate(audit['rows']): + failures = list(errors) + close(row['viewport_rect'], affine_rect(request['local_to_viewport'], row['rect']), + 'frame viewport rectangle') + close(row['screen_rect'], affine_rect(request['local_to_screen'], row['rect']), + 'frame content rectangle') + status, clipped = rectangle_eligibility(row['rect'], report['viewport']) + screenshot_rect = affine_rect(calibration, affine_rect(request['local_to_screen'], clipped)) + room = report['rooms'].get(row['id']) if row['category'] == 'room' else None + if row['category'] in ('business', 'goods'): + room = report['rooms'].get(report['badges'][int(row['id'])]['facility']) + operation, state = None, None + if room is not None: + states = enum_names(root, 'scripts/facility/facility_instance.gd', 'State') + operations = enum_names(root, 'scripts/facility/facility_instance.gd', 'OperationState') + require(type(room['state']) is int and 0 <= room['state'] < len(states), 'frame room enum') + state = states[room['state']] + require(state == room['state_name'], 'frame room state name') + require(type(room['operation']) is int and 0 <= room['operation'] < len(operations), 'frame operation enum') + operation = operations[room['operation']] + actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']] + actor = next((a for a in actors if a['key'] == row['id']), None) + if actor is not None: + actor_states = enum_names(root, 'scripts/staff/staff_runtime.gd' if actor['staff'] + else 'scripts/guest/guest_runtime.gd', 'State') + require(type(actor['state']) is int and 0 <= actor['state'] < len(actor_states), + 'frame actor state enum') + state = actor_states[actor['state']] + if row['category'] == 'queue': + badge = report['landing_badges'][int(row['id'])] + state = {k: badge[k] for k in ('members', 'visible_count', 'overflow')} + if row['category'] == 'aggregation': + badge = report['actor_aggregations'][int(row['id'])] + state = {k: badge[k] for k in ('members', 'drawn_keys')} + if row['category'] == 'EV': + cabin = next(c for c in report['cabins'] if c['shaft_id'] == row['id']) + state = {k: cabin[k] for k in ('operational', 'doors_open')} + if row['category'] == 'traffic': + traffic = next(r['traffic'] for r in report.get('traffic', []) + if ','.join(map(str, r['cell'])) == row['id']) + state = {k: traffic[k] for k in ('current_density', 'temporary_block')} + result = dict(row_index=index, category=row['category'], id=row['id'], layer=report['layer'], + state=state, state_name=state if isinstance(state, str) else None, operation=operation, + dimmed=row['category'] == 'room' and operation != 'OPERATING', + text=row.get('text'), local_rect=row['rect'], screenshot_rect=screenshot_rect, + style_source=row['style_source'], style_source_sha256=sha(root/row['style_source']), + style_colors=row['style_colors'], font_width=row.get('font_width'), + eligibility=status, reason={'visible': 'whole rectangle in viewport', + 'clipped': 'rectangle intersects viewport', 'outside': 'no viewport intersection'}[status], + crops=[]) + if status != 'outside': + for number, (width, height, pixels) in enumerate(images): + x, y, w, h = rect_intersection(screenshot_rect, [0, 0, width, height]) + crop = [line[math.ceil(x):math.floor(x+w)] + for line in pixels[math.ceil(y):math.floor(y+h)]] + if not crop or not crop[0]: + failures.append('eligible crop outside image') + continue + if len({pixel for line in crop for pixel in line}) == 1: + failures.append('blank crop') + data = encode_png(crop) + result['crops'].append(dict(image_index=number, sha256=hashlib.sha256(data).hexdigest(), + png_bytes=data)) + result['mechanical_pass'] = not failures + result['failures'] = failures + rows.append(result) + return dict(rows=rows, errors=errors, mechanical_pass=not errors and all(r['mechanical_pass'] for r in rows), + expected_row_count=len(drawing_rectangle_rows(report)), + transit_authority=transit_authority) + + +def write_frame_review(directory, frame, provenance): + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + manifest = copy.deepcopy(frame) + manifest['images'] = provenance + for row in manifest['rows']: + for crop in row['crops']: + name = 'row-%04d-image-%d.png' % (row['row_index'], crop['image_index']) + (directory/name).write_bytes(crop.pop('png_bytes')) + crop['path'] = name + data = (json.dumps(manifest, sort_keys=True, ensure_ascii=False, indent=2) + '\n').encode() + (directory/'manifest.json').write_bytes(data) + digest = hashlib.sha256(data).hexdigest() + (directory/'manifest.sha256').write_text(digest + '\n') + return manifest, digest + + +def reviewed_pixel_categories(manifest, digest, verdict_path, *, require_judgments=False): + """The worker never writes verdicts; absent independent review earns nothing.""" + if not Path(verdict_path).is_file(): + return set() + verdict = json.loads(Path(verdict_path).read_bytes()) + require(verdict['manifest_sha256'] == digest, 'review manifest SHA mismatch') + require(verdict['reviewer'] == 'independent-opus-direct-image', 'independent image reviewer required') + if require_judgments: + require(isinstance(verdict.get('judgments'), list), 'explicit indexed judgments required') + verdict = dict(verdict, rows=verdict['judgments']) + eligible = [r for r in manifest['rows'] if r['eligibility'] != 'outside'] + require([v['row_index'] for v in verdict['rows']] == [r['row_index'] for r in eligible], + 'review row coverage') + for row, decision in zip(eligible, verdict['rows']): + require(all(decision[k] is True for k in ('category', 'state', 'position')), 'review row rejected') + if not manifest['mechanical_pass']: + return set() + return {r['category'] for r in eligible if r['mechanical_pass']} + + +def mirrored_inventory(directory): + """Read every indexed byte; preserve original receipt paths as authority.""" + directory = Path(directory).resolve() + index = json.loads((directory/'MIRROR_INDEX.json').read_bytes()) + require(Path(index['mirror']).resolve() == directory, 'mirror root mismatch') + paths, mapping = set(), [] + source = Path(index['source']) + for row in index['files']: + name = Path(row['path']) + require(not name.is_absolute() and '..' not in name.parts and str(name) not in paths, + 'invalid/duplicate mirror path') + paths.add(str(name)) + local = directory/name + require(local.resolve().is_relative_to(directory) and not local.is_symlink(), 'mirror escape') + data = local.read_bytes() + digest = hashlib.sha256(data).hexdigest() + require(digest == row['sha256'] and len(data) == row['bytes'], 'mirror hash/size mismatch') + mapping.append(dict(original=str(source/name), mirror=str(local), sha256=digest)) + prior = index['previous_inventory'] + prior_name = Path(prior['path']) + require(not prior_name.is_absolute() and '..' not in prior_name.parts + and (directory/prior_name).resolve().is_relative_to(directory) + and not (directory/prior_name).is_symlink(), 'previous inventory escape') + require(sha(directory/prior['path']) == prior['sha256'], 'previous mirror inventory hash') + old = json.loads((directory/prior['path']).read_bytes()) + require(all(row in index['files'] for row in old['files']), 'previous mirror entries changed') + return index, mapping + + +def inspect_offline_mirror(directory): + """Preparation diagnostics, deliberately separate from the acceptance gate. + + Frozen incomplete draw audits cannot acquire missing rows by relabelling + source-derived rectangles as observed engine output. + """ + directory = Path(directory).resolve() + index, mapping = mirrored_inventory(directory) + by_original = {row['original']: row for row in mapping} + def resolve(original): + require(original in by_original, 'unindexed original path') + return Path(by_original[original]['mirror']) + pair = directory/'os-pair/A' + clicks = [json.loads((pair/name/'cua-receipt.json').read_bytes()) + for name in ('writer-request-0001', 'writer-request-0003')] + results, inspected = [], [] + for name in ('writer-request-0002', 'writer-request-0004'): + folder = pair/name + request = json.loads((folder/'request.json').read_bytes()) + receipt = json.loads((folder/'cua-receipt.json').read_bytes()) + require(sha(folder/'request.json') == receipt['request_sha256'], 'offline request binding') + require(receipt['target_pid'] == request['target_pid'], 'offline screenshot PID') + report, audit = decode(request['report']), decode(request['rectangles']) + images = [] + for number, suffix in enumerate(('', '-after')): + conversion_path = folder/('root-early-conversion'+suffix+'.json') + conversion = json.loads(conversion_path.read_bytes()) + shot = receipt['screenshots'][number] + require(resolve(conversion['raw_path']) == directory/shot['path'], 'offline raw path binding') + image = inspect_conversion(conversion, shot, resolve_path=resolve) + for stream in ('stdout', 'stderr'): + require(sha(Path(str(conversion_path)+'.'+stream)) == conversion[stream+'_sha256'], + 'offline converter log hash') + images.append(image) + for original in (str(Path(index['source'])/conversion_path.relative_to(directory)), + conversion['png_path']): + inspected.append(by_original[original]) + stream = pair/'writer-stream' + captures = [row for row in map(json.loads, (stream/'manifest.jsonl').read_text().splitlines()) + if row.get('label') == name and 'report' in row] + require(len(captures) == 1, 'offline raw-state capture coverage') + capture = captures[0] + state, rest_audit = drawing_context(stream, capture, report) + frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT, + state, rest_audit) + provenance = [] + for number, suffix in enumerate(('', '-after')): + conversion_path = folder/('root-early-conversion'+suffix+'.json') + conversion = json.loads(conversion_path.read_bytes()) + provenance.append(dict(whole_image_sha256=conversion['png_sha256'], + raw_sha256=conversion['raw_sha256'], + conversion_sha256=sha(conversion_path), + request_sha256=sha(folder/'request.json'), + receipt_sha256=sha(folder/'cua-receipt.json'))) + review_dir = ROOT/'docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review'/name + manifest, digest = write_frame_review(review_dir, frame, provenance) + credited = reviewed_pixel_categories(manifest, digest, review_dir/'independent-verdict.json', require_judgments=True) + present = {r['category'] for r in frame['rows'] if r['eligibility'] != 'outside'} + overlays = [dict(cell=r['cell'], rect=r['rect']) for r in report['traffic'] + if (r['traffic']['current_density'] > 0 or r['traffic']['temporary_block']) + and not any(a['category'] == 'traffic' and a['id'] == ','.join(map(str, r['cell'])) + for a in audit['rows'])] + results.append(dict(request=name, layer=report['layer'], manifest_sha256=digest, + manifest_path=str(review_dir/'manifest.json'), rows=manifest['rows'], + mechanical_pass=frame['mechanical_pass'], errors=frame['errors'], + unaudited_traffic_overlays=overlays, + pending_categories=[c for c in CATEGORIES if c not in present], + pixel_categories=sorted(credited), independent_review='pending')) + return dict(task_id='P23-03', acceptance='pending', status='blocked', + inventory_count=len(mapping), path_map=mapping, inspected_eight=inspected, + frames=results, root_native_os_full_regression_independent_review='pending') + + +def enum_names(root, relative, name): + source = (root / relative).read_text() + match = re.search(r'\benum\s+' + re.escape(name) + r'\s*\{([^}]+)\}', source) + require(match is not None, 'missing source enum ' + name) + names = [x.strip() for x in match[1].split(',') if x.strip()] + require(all(re.fullmatch(r'[A-Z][A-Z_0-9]*', x) for x in names), 'unsupported enum schema') + require(len(set(names)) == len(names), 'duplicate source enum') + return names + + +def projection(rows, mapping, states): + result = [] + ids = set() + for row in rows: + value = {target: copy.deepcopy(row[source]) for target, source in mapping.items()} + require(value['id'] not in ids, 'duplicate actor/task ID') + ids.add(value['id']) + require(value['state'] in states, 'unknown typed state') + result.append(value) + return result + + +def compare_actor_rows(snapshot, report, root=ROOT): + runtime = snapshot['systems']['runtime'] + guest_states = enum_names(root, 'scripts/guest/guest_runtime.gd', 'State') + staff_states = enum_names(root, 'scripts/staff/staff_runtime.gd', 'State') + task_states = enum_names(root, 'scripts/staff/staff_task.gd', 'State') + mappings = ( + ('guests', runtime['guests']['guests'], guest_states, + dict(id='guest_id', state='state', cell='current_cell', target='target_facility_id', + service='current_service_id', queue='queue_request_id')), + ('staff', runtime['staff']['staff'], staff_states, + dict(id='staff_id', state='state', cell='current_cell', task='current_task_id', + facility='assigned_facility_id', role='role_id', carried_inventory_id='carried_inventory_id')), + ('tasks', runtime['staff']['tasks'], task_states, + dict(id='task_id', state='state', type='task_type', staff='assigned_staff_id', + target='target_entity_id', rescue_facility='rescue_facility_id')), + ) + for key, rows, states, mapping in mappings: + expected = projection(rows, mapping, states) + require(report[key] == expected, 'typed projection mismatch: ' + key) + return [key for key, *_ in mappings] + + +def elevator_report(snapshot): + saved = snapshot['systems']['runtime']['elevators'] + # ElevatorService.restore_snapshot validates this exact trip shape (412-438). + # QUEUED means no cabin seat yet, not an absent shaft assignment. + configs = {shaft['config']['shaft_id']: shaft['config'] for shaft in saved['shafts']} + for trip in saved['trips']: + keys = {'actor_id', 'request_id', 'shaft_id', 'connector_id', 'from', 'to', + 'enqueued_at', 'state'} + if 'companion_id' in trip: + keys.add('companion_id') + require(isinstance(trip['companion_id'], str) and trip['companion_id'], + 'invalid elevator companion identity') + require(set(trip) == keys, 'exact elevator trip keys') + require(trip['shaft_id'] in configs, 'elevator trip shaft attribution') + config = configs[trip['shaft_id']] + require(trip['from'] != trip['to'] and trip['from'] in config['stops'] + and trip['to'] in config['stops'], 'elevator trip stops') + low, high = sorted((trip['from'][1], trip['to'][1])) + require(trip['connector_id'] == f"{trip['shaft_id']}.link.{low}.{high}", + 'elevator trip connector attribution') + require(trip['state'] in ('QUEUED', 'BOARDED'), 'elevator trip state') + rows = [] + for shaft in saved['shafts']: + row = copy.deepcopy(shaft) + row.update(shaft['config']) + floor = float(shaft['current_floor']) + if shaft['phase'] in ('PICKUP', 'TRAVEL'): + destination = shaft['boarding_floor'] if shaft['phase'] == 'PICKUP' else shaft['target_floor'] + progress = 1.0 - shaft['remaining_minutes'] / max(float(shaft['phase_total_minutes']), 1.0) + floor = shaft['phase_origin_floor'] + (destination - shaft['phase_origin_floor']) * progress + row.update(visual_floor=floor, passenger_count=len(shaft['boarded']), + passenger_ids=copy.deepcopy(shaft['boarded']), + # get_report() merges config before counting (345-359). + waiting_count=sum(t['shaft_id'] == row['shaft_id'] and t['state'] == 'QUEUED' + for t in saved['trips'])) + rows.append(row) + # WorldPreview sorts its report's trip copy for queue placement. + trips = sorted(copy.deepcopy(saved['trips']), key=lambda t: (t['enqueued_at'], t['actor_id'])) + return dict(current_minute=saved['current_minute'], shafts=rows, trips=trips) + + +def close(actual, expected, label): + if isinstance(expected, (float, int)) and not isinstance(expected, bool): + require(type(actual) in (int, float) and math.isfinite(actual) + and math.isclose(actual, expected, abs_tol=0.0001, rel_tol=0.000001), label) + elif isinstance(expected, dict): + require(isinstance(actual, dict) and actual.keys() == expected.keys(), label + ' keys') + for key in expected: + close(actual[key], expected[key], label + '/' + str(key)) + elif isinstance(expected, list): + require(isinstance(actual, list) and len(actual) == len(expected), label + ' length') + for a, e in zip(actual, expected): + close(a, e, label) + else: + require(type(actual) is type(expected) and actual == expected, label) + + +class Transform: + """Independent port of the shared cell/map transform; no report rect input.""" + def __init__(self, runtime, report): + self.bounds = runtime['world']['bounds'] + b = self.bounds + columns, rows = b['max_x'] - b['min_x'] + 1, b['max_y'] - b['min_y'] + 1 + width, height = report['viewport'] + require(width > 0 and height > 0 and report['zoom'] > 0, 'camera dimensions') + fit = min(max(width - 152, 1) / (columns * 16), max(height - 140, 1) / (rows * 24)) + pixel = max(math.floor(fit), 1) if fit >= 1 else max(fit, .25) + self.dim = [max(math.floor(v * pixel * report['zoom'] + .5), minimum) + for v, minimum in ((16, 4), (24, 6))] + self.origin = [math.floor((extent - count * dim) / 2 + pan) + for extent, count, dim, pan in zip((width, height), (columns, rows), self.dim, report['pan'])] + + def rect(self, cell): + b = self.bounds + return [self.origin[0] + (cell[0] - b['min_x']) * self.dim[0], + self.origin[1] + (b['max_y'] - cell[1]) * self.dim[1], *self.dim] + + def center(self, cell): + x, y, w, h = self.rect(cell) + return [x + w / 2, y + h / 2] + + def room(self, cells): + rects = [self.rect(c) for c in cells] + x, y = min(r[0] for r in rects), min(r[1] for r in rects) + return [x, y, max(r[0] + r[2] for r in rects) - x, max(r[1] + r[3] for r in rects) - y] + + +def badge_rectangle(at, text): + return list(at) + [len(text) * 10 + 6, 14] + + +def observation_label_layout(report): + """Independent ordered rectangle placement; never consume reported at/reasons.""" + placed, result = [], [] + for i, cabin in enumerate(report['cabins']): + placed.extend([(f'cabin/{i}/rect', cabin['rect']), + (f'cabin/{i}/badge', badge_rectangle(cabin['badge_at'], cabin['badge']))]) + for kind, collection in [('business', 'badges'), ('supply', 'badges'), + ('landing', 'landing_badges'), ('aggregation', 'actor_aggregations')]: + for i, row in enumerate(report.get(collection, [])): + if collection == 'badges' and row['kind'] != kind: + continue + anchor = list(row.get('anchor', row['at'])) + at, reasons = anchor[:], [] + while True: + conflict = next(((identity, rect) for identity, rect in placed + if intersects(badge_rectangle(at, row['text']), rect)), None) + if conflict is None: + break + identity, rect = conflict + reasons.append(identity) + at[0] = rect[0] + rect[2] + result.append((collection, i, dict(anchor=anchor, at=at, displaced_by=reasons))) + placed.append((f'{kind}/{i}', badge_rectangle(at, row['text']))) + return result + + +def observation_anchor_report(report): + """Projection for existing raw-state semantic recomputation, not evidence.""" + import copy + raw = copy.deepcopy(report) + for collection in ('badges', 'landing_badges', 'actor_aggregations'): + for row in raw.get(collection, []): + if 'anchor' in row: + row['at'] = row.pop('anchor') + row.pop('displaced_by', None) + return raw + + +def validate_observation_labels(report): + require(all(key in report for key in ('cabins', 'badges', 'landing_badges', 'actor_aggregations')), + 'missing observation label collections') + rectangles = [badge_rectangle(c['badge_at'], c['badge']) for c in report['cabins']] + rectangles += [badge_rectangle(b['at'], b['text']) + for key in ('badges', 'landing_badges', 'actor_aggregations') + for b in report.get(key, [])] + for i, rect in enumerate(rectangles): + require(not any(intersects(rect, other) for other in rectangles[:i]), + 'observation label intersection') + for collection, index, wanted in observation_label_layout(report): + row = report[collection][index] + for key, value in wanted.items(): + require(key in row, 'missing label ' + key) + close(row[key], value, 'observation label/' + key) + + +def intersects(a, b): + cut = rect_intersection(a, b) + return cut[2] > 0 and cut[3] > 0 + + +def validate_cabin_intrusion(report): + for cabin in report['cabins']: + require(len(cabin['passengers']) <= cabin['passenger_count'], 'passenger drawn in empty cabin') + for actor in report.get('actor_drawings', []): + require(not intersects(actor_identity_core(actor), cabin['rect']), + 'non-passenger inside cabin: ' + actor['key']) + + +def traffic_obstacles(report): + obstacles = [] + for cabin in report['cabins']: + rect = badge_rectangle(cabin['badge_at'], cabin['badge']) + close(cabin['badge_rect'], rect, 'cabin badge rectangle') + obstacles.extend([cabin['rect'], rect]) + for collection in ('badges', 'landing_badges', 'actor_aggregations'): + obstacles.extend(badge_rectangle(b['at'], b['text']) for b in report.get(collection, [])) + return obstacles + + +def traffic_segments(band, cell, obstacles): + # Independent rectangle-set subtraction, including the deterministic fallback + # for a band entirely behind a cabin. Never accept self-asserted no-overlap. + for y in (band[1], cell[1], cell[1] + cell[3] - band[3]): + pieces = [[band[0], y, band[2], band[3]]] + for obstacle in obstacles: + remaining = [] + for x, top, w, h in pieces: + cut = rect_intersection([x, top, w, h], obstacle) + if not cut[2] or not cut[3]: + remaining.append([x, top, w, h]) + continue + left, upper, cw, ch = cut + remaining.extend(r for r in ([x, top, w, upper-top], + [x, upper+ch, w, top+h-upper-ch], [x, upper, left-x, ch], + [left+cw, upper, x+w-left-cw, ch]) if r[2] > 0 and r[3] > 0) + pieces = remaining + if pieces: + return pieces + return [] + + +def compare_traffic(snapshot, report): + runtime = snapshot['systems']['runtime'] + transform = Transform(runtime, report) + rows = [] + for saved in sorted(runtime['path']['cell_traffic'], key=lambda r: (r['cell'][1], r['cell'][0])): + cell = saved['cell'] + if cell[2] != report['layer']: + continue + x, y, w, h = transform.rect(cell) + rect = [x + 2, y + 2, w - 4, h - 4] + thickness = max(2, min(5, (h - 4) * .16)) + band = [rect[0] + rect[2] * .12, y + h - 2 * (w / 16) - thickness / 2, + rect[2] * .76, thickness] + rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'}, + rect=rect, band_rect=band, + segment_rects=traffic_segments(band, rect, traffic_obstacles(report)))) + close(report['traffic'], rows, 'traffic and cell rect') + validate_life_overlay(report, snapshot=snapshot) + + +def validate_life_overlay(report, root=ROOT, snapshot=None): + """Check consumed frame and source wiring, not merely reported order flags.""" + serial = report.get('observation_serial') + require(type(serial) is int and serial > 0 + and type(report.get('overlay_consumed_serial')) is int + and report['overlay_consumed_serial'] == serial, 'stale LifeOverlay serial') + require(report.get('overlay_passes') == ['traffic', 'cabins', 'actors'], 'LifeOverlay pass order') + indices = report.get('overlay_child_indices', []) + require(len(indices) == 3 and all(type(i) is int and i >= 0 for i in indices) + and indices[0] < indices[1] < indices[2], 'LifeOverlay child order') + source = (root / 'scenes/main/world_preview.gd').read_text() + def body(name): + match = re.search(r'^func ' + name + r'\([^\n]*\n(.*?)(?=^func |\Z)', source, re.M | re.S) + require(match is not None, 'missing overlay source function: ' + name) + return match[1] + ready, draw, overlay = body('_ready'), body('_draw_contents'), body('_draw_life_overlay') + ordered = [ready.find('add_child(' + node + ')') + for node in ('_inactive_layer_art', '_life_overlay', '_layer_lens')] + require(all(i >= 0 for i in ordered) and ordered == sorted(set(ordered)), 'LifeOverlay source child order') + require('_life_overlay.draw.connect(_draw_life_overlay)' in ready + and '_life_overlay.queue_redraw()' in draw, 'LifeOverlay source connection') + require('_draw_congestion_overlay(' not in draw and '_draw_actors(' not in draw + and '_draw_observed_actor(' not in body('_draw_elevators'), 'actor or band drawn in parent') + require(0 <= overlay.find('_draw_congestion_overlay(_life_overlay)') + < overlay.find('_draw_elevators(_life_overlay)') + < overlay.find('_draw_life_badges(_life_overlay)') + < overlay.find('_draw_observed_actor(actor, _life_overlay)') + < overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order') + require('canvas.draw_rect(rect, Color("34413b"))' in body('_draw_readable_badge') + and 'canvas.draw_rect(row[&"band_rect"], Color(color, 0.90))' in body('_draw_congestion_overlay'), + 'occluded traffic requires opaque later badges and an actual band draw') + require('text.length() * 10 + 6' in body('_readable_badge_rect') + and '_readable_badge_rect(at, text)' in body('_draw_readable_badge') + and 'PixelArt.elevator_cabin(canvas,' in body('_draw_elevators') + and 'PixelArt.elevator_gate(canvas,' in body('_draw_elevators') + and '_draw_elevators(' not in draw, 'cabin/badge draw geometry or active-layer canvas differs') + actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']] + # Projection authority comes from raw state, never riding/region flags. + cabin_actors, carried_keys = set(), set() + if snapshot is not None: + if report['cabins']: + compare_cabin_geometry(snapshot, report) + shafts = {s['shaft_id']: s for s in elevator_report(snapshot)['shafts']} + for cabin in report['cabins']: + members = {key.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1) + for key in shafts[cabin['shaft_id']]['boarded']} + cabin_actors.update(id(a) for a in cabin['passengers'] if a['key'] in members) + tasks = snapshot['systems']['runtime']['staff']['tasks'] + carried_keys = {'staff/' + t['target_entity_id'] for t in tasks + if t['task_type'] == 'rescue_transport' + and t['state'] not in ('WORKING', 'COMPLETED', 'CANCELLED', 'FAILED') + and t['assigned_staff_id']} + carried_keys.difference_update(report.get('boarded', {}), report.get('queue_positions', {})) + if carried_keys: + # Reuse the original raw binding, track and raised-region fit checks. + compare_actor_geometry(snapshot, report) + obstacles = traffic_obstacles(report) + close(report['traffic_obstacles'], obstacles, 'traffic obstacle geometry') + validate_cabin_intrusion(report) + for row in report.get('traffic', []): + x, y, w, h = row['rect'] + thickness = max(2, min(5, h * .16)) + expected = [x + w * .12, y + h + 2 - 2 * ((w + 4) / 16) - thickness / 2, + w * .76, thickness] + close(row['band_rect'], expected, 'traffic band rectangle') + segments = traffic_segments(expected, row['rect'], obstacles) + close(row['segment_rects'], segments, 'traffic segment geometry') + require(all(not intersects(part, obstacle) for part in segments for obstacle in obstacles), + 'traffic intersects cabin or badge') + if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']: + continue + intervals = [] + for actor in actors: + core = actor_identity_core(actor) + overlaps = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2] + and core[1] < expected[1] + expected[3] and expected[1] < core[1] + core[3]) + require(not overlaps or id(actor) in cabin_actors + or (actor in report['actor_drawings'] and actor['key'] in carried_keys), + 'floor actor core over traffic band: ' + actor['key']) + # The relocated fragments also cannot cross an ordinary floor core. + require(not any(intersects(core, part) for part in segments) + or id(actor) in cabin_actors or actor['key'] in carried_keys, + 'floor actor core over traffic segment: ' + actor['key']) + if overlaps: + intervals.append((max(core[0], expected[0]), + min(core[0] + core[2], expected[0] + expected[2]))) + end = expected[0] + for left, right in sorted(intervals): + if left > end: + break + end = max(end, right) + require(end < expected[0] + expected[2], + 'traffic band fully occluded: ' + str(row.get('cell'))) + + +def compare_queues(snapshot, report): + if any('anchor' in b for b in report.get('landing_badges', [])): + validate_observation_labels(report) + report = observation_anchor_report(report) + runtime = snapshot['systems']['runtime'] + transform = Transform(runtime, report) + facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id') + requests = codec.indexed(runtime['facilities']['requests'], 'request_id') + guests = codec.indexed(runtime['guests']['guests'], 'guest_id') + groups = {} + def add(key, cell): + if cell[2] != report['layer']: + return + members = groups.setdefault(tuple(cell), []) + if key not in members: + members.append(key) + for facility in facilities.values(): + for rid in facility['queue_entries']: + request = requests.get(rid) + if request is None: + continue + for gid in request['guest_ids']: + guest = guests.get(gid) + if guest and guest['state'] == 'QUEUING': + add('guest/' + gid, guest['current_cell']) + elevators = elevator_report(snapshot) + for trip in elevators['trips']: + if trip['state'] == 'QUEUED': + add(trip['actor_id'].replace('guest:', 'guest/', 1).replace('staff:', 'staff/', 1), trip['from']) + footprint = list(dict.fromkeys((cell[0] + dx, cell[1], cell[2]) + for shaft in elevators['shafts'] for cell in shaft['stops'] + for dx in range(max(int(shaft.get('width', 2)), 1)))) + close(report['shaft_footprint'], [list(c) for c in footprint], 'shaft footprint') + lift_cells = set(footprint) + walkable = {tuple(cell) for cell in runtime['world']['walkable_cells']} + owners = {tuple(cell): row['entity_id'] for row in runtime['world']['occupancies'] for cell in row['cells']} + reserved, positions, hidden, badges = set(), {}, {}, [] + for cell in sorted(groups, key=lambda c: (c[1], c[0])): + members = groups[cell] + lane = [] if cell in lift_cells else [cell] + for direction in (-1, 1): + for distance in range(1, 9): + target = (cell[0] + direction * distance, cell[1], cell[2]) + facility = facilities.get(owners.get(target)) + if target in lift_cells or target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')): + break + lane.append(target) + index = 0 + for target in lane: + if index >= len(members): + break + if target in reserved: + continue + reserved.add(target) + positions[members[index]] = transform.center(target) + index += 1 + hidden.update({member: True for member in members[index:]}) + overflow = len(members) - index + rect = transform.rect(cell) + badges.append(dict(at=[rect[0] + (-80 if cell in lift_cells else 0), rect[1] - 16], + text=f'待ち{len(members)}人' + (f'(他{overflow}人)' if overflow else ''), + cell=list(cell), members=members, visible_count=index, overflow=overflow)) + for key, expected in [('queue_positions', positions), ('queue_hidden', hidden), ('landing_badges', badges)]: + close(report[key], expected, key) + + +def compare_room_services(snapshot, report, root=ROOT): + """Check authoritative service occupants and reasons; rest selection remains pending. + + Extra rest/rescue occupants are checked separately below, never silently + counted as service users. This does not claim the missing path-cost proof. + """ + runtime = snapshot['systems']['runtime'] + states = enum_names(root, 'scripts/facility/facility_instance.gd', 'State') + operations = enum_names(root, 'scripts/facility/facility_instance.gd', 'OperationState') + rooms = [r for r in runtime['facilities']['instances'] if r['origin'][2] == report['layer']] + treated, carried = {}, {} + for task in runtime['staff']['tasks']: + if task['task_type'] != 'rescue_transport' or task['state'] in ('COMPLETED', 'CANCELLED', 'FAILED') or not task['assigned_staff_id']: + continue + if task['state'] == 'WORKING': + treated[task['target_entity_id']] = task['rescue_facility_id'] + else: + carried[task['target_entity_id']] = task['assigned_staff_id'] + close(report['treated_patients'], treated, 'rescue treatment') + close(report['carried_patients'], carried, 'rescue carrier') + staff = codec.indexed(runtime['staff']['staff'], 'staff_id') + all_rooms = codec.indexed(runtime['facilities']['instances'], 'instance_id') + for sid, fid in report['resting_staff'].items(): + require(sid in staff and fid in all_rooms, 'rest reference') + member, room = staff[sid], all_rooms[fid] + require(member['state'] == 'OFF_DUTY' and member['health'] > 0 and not member['current_task_id'], 'resting staff eligibility') + require(room['definition_id'] in ('staff_waiting_room', 'staff_dormitory') + and room['state'] in ('OPEN', 'DEGRADED') and not room['suspension_reason'], 'operating rest room') + require(set(report['rooms']) == {r['instance_id'] for r in rooms}, 'room identity') + for room in rooms: + observed = report['rooms'][room['instance_id']] + users = sorted({gid for service in room['active_services'] for gid in service['guest_ids']}) + users += [sid for sid in staff if report['resting_staff'].get(sid) == room['instance_id']] + users += [task['target_entity_id'] for task in runtime['staff']['tasks'] + if task['task_type'] == 'rescue_transport' and task['state'] == 'WORKING' + and task['assigned_staff_id'] and task['rescue_facility_id'] == room['instance_id']] + require(observed['people'] == users, 'service/rest/rescue occupant list') + require(observed['count'] == len(observed['people']), 'room count') + operation = 'SUSPENDED' if room['suspension_reason'] else ('OPERATING' if room['state'] in ('OPEN', 'DEGRADED') else 'INACTIVE') + expected = dict(state=states.index(room['state']), state_name=room['state'], + reasons=room['state_reasons'], operation=operations.index(operation), + limit=room['customer_capacity'], staff=room['active_staff_ids'], suspension=room['suspension_reason']) + for key, value in expected.items(): + close(observed[key], value, 'room/' + key) + cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == room['instance_id']), []) + if cells: + close(observed['rect'], Transform(runtime, report).room(cells), 'room rect') + require(observed['definition_id'] == room['definition_id'], 'room definition') + + +def comparison_records(directory): + records = {} + for line in (directory / 'manifest.jsonl').read_text().splitlines(): + row = json.loads(line) + if 'object' not in row: + continue + value = read_object(directory, row['object']) + if row['label'] == 'event': + continue + require(row['label'] not in records, 'duplicate experiment record') + records[row['label']] = value + return records + + +def room_slots(rect, pixel, definition='staff_waiting_room'): + x, y, w, h = rect + step = 20 if definition in ('cafeteria', 'tavern') else 16 + columns = max(int(w / pixel / step), 1) + rows = max(int((h / pixel - (30 if definition == 'chapel' else 10)) / 20), 1) + if definition == 'chapel': + columns = min(columns, 4) + anchors = [[x + (col + .5) * w / columns, y + h - (4 + row * 20) * pixel] + for row in range(rows) for col in range(columns)] + return anchors[:8] if definition == 'chapel' else anchors + + +def compare_rest(state, report, audit, root=ROOT): + """Recompute selection, not shortest paths. Shortest-path authority is the + exact pinned PathService source producing the recorded public API paths. + Edge reports must be nonempty, including for temporarily closed cells. + """ + require(audit['path_service_sha256'] == sha(root / 'scripts/world/path_service.gd'), + 'rest PathService source pin') + runtime = state['systems']['runtime'] + members = runtime['staff']['staff'] + facilities = runtime['facilities']['instances'] + require([r['staff_id'] for r in audit['staff']] == [r['staff_id'] for r in members], + 'rest staff order/coverage') + passable = {tuple(r['cell']) for r in runtime['path']['cell_traffic']} + resting = {} + selections = {} + for member, row in zip(members, audit['staff']): + sid = member['staff_id'] + for key in ('state', 'health', 'current_task_id', 'current_cell'): + close(row[key], member[key], 'rest input/' + key) + memberships = [dict(raid_id=raid['raid_id'], state=raid['state'], side=c['side']) + for raid in runtime['defense']['raids'] for c in raid['combatants'] + if c['actor_id'] == sid] + close(row['defense_memberships'], memberships, 'rest defense membership') + locked = sid in runtime['events']['actor_locks'] + transit = any(t['actor_id'] == 'staff:' + sid for t in runtime['elevators']['trips']) + close(row['actor_lock'], locked, 'rest actor lock') + close(row['in_transit'], transit, 'rest transit') + defense = any(r['state'] not in ('DEFENDED', 'BREACHED', 'DEFENDERS_RETREATED', 'CANCELLED') + and r['side'] == 'DEFENDER' for r in memberships) + eligible = member['state'] == 'OFF_DUTY' and member['health'] > 0 and not member['current_task_id'] and not locked and not defense + selected = dict(reachable=False, resting=False, label='休憩先なし(稼働中の待機室・宿舎と経路を確認)') if eligible else {} + best_cost = 2147483647 + require([f['instance_id'] for f in row['facilities']] == [f['instance_id'] for f in facilities], + 'rest facility order/coverage') + for facility, observed in zip(facilities, row['facilities']): + for key in ('instance_id', 'definition_id', 'origin', 'occupied_cells'): + close(observed[key], facility[key], 'rest facility/' + key) + operation = 'SUSPENDED' if facility['suspension_reason'] else ('OPERATING' if facility['state'] in ('OPEN', 'DEGRADED') else 'INACTIVE') + close(observed['operation_state'], operation, 'rest operation') + occupied = facility['occupied_cells'] or [facility['origin']] + probes = [dict(cell=[c[0] + dx, c[1], c[2]], has_cell=(c[0] + dx, c[1], c[2]) in passable) + for c in occupied for dx in (0, -1, 1)] + close(observed['probes'], probes, 'rest candidate probes') + ordered = sorted({tuple(p['cell']) for p in probes if p['has_cell']}, key=lambda c: (c[2], c[1], c[0])) + close(observed['ordered'], [list(c) for c in ordered], 'rest candidate order') + require([r['cell'] for r in observed['routes']] == [list(c) for c in ordered], 'rest route coverage/order') + best = None + for route in observed['routes']: + path, edges = route['path'], route['edges'] + require(len(edges) == max(len(path) - 1, 0), 'rest edge coverage') + if not path: + require(route['calculate_path_cost'] == -1, 'rest empty route cost') + continue + require(path[0] == member['current_cell'] and path[-1] == route['cell'], 'rest path endpoints') + require(all(tuple(c) in passable for c in path), 'rest path cell absent') + cost = 0 + for a, b, edge in zip(path, path[1:], edges): + require(edge and edge['from'] == a and edge['to'] == b, 'rest empty or discontinuous edge report') + require(type(edge['total_cost']) is int and edge['total_cost'] >= 0, 'rest edge cost') + cost += edge['total_cost'] + require(cost == route['calculate_path_cost'], 'rest edge sum differs from calculate_path_cost') + if best is None or cost < best[0]: + best = (cost, path[-1]) + if not eligible or operation != 'OPERATING' or facility['definition_id'] not in ('staff_waiting_room', 'staff_dormitory') or best is None or best[0] >= best_cost: + continue + best_cost = best[0] + cell = member['current_cell'] + at = any(c[2] == cell[2] and c[1] == cell[1] and abs(c[0] - cell[0]) <= 1 for c in occupied) and not transit + selected = dict(reachable=True, resting=at, cell=best[1], facility_id=facility['instance_id'], + label=('宿舎で休憩中' if facility['definition_id'] == 'staff_dormitory' else '待機室で休憩中') if at else '休憩先へ移動中') + close(row['comparison_only'], selected, 'rest selection comparison') + selections[sid] = selected + if selected.get('resting'): + resting[sid] = selected['facility_id'] + close(report['resting_staff'], resting, 'draw resting_staff') + # Fitting is cross-kind, and queue precedence applies to resting staff too. + # Reconstruct the complete group and independently prove p=1 no-fit before + # allowing an identity to appear only in an aggregation's member list. + if any(r['cells'] for r in runtime['world']['occupancies'] + if r['entity_id'] in resting.values()): + compare_actor_geometry(state, report) + return selections + + +def compare_goods(snapshot, report, root=ROOT): + runtime = snapshot['systems']['runtime'] + states = enum_names(root, 'scripts/inventory/logistics_request.gd', 'State') + candidates = codec.indexed(json.loads((root / 'data/characters/mvp_staff_candidates.json').read_text())['entries'], 'id') + staff = codec.indexed(runtime['staff']['staff'], 'staff_id') + facilities = runtime['facilities']['instances'] + def owner(inventory): + return next((f['instance_id'] for f in facilities if f['local_inventory_id'] == inventory), '') + rows, active, recent = [], [], [] + for request in sorted(runtime['logistics_requests'], key=lambda r: r['request_id']): + require(request['state'] in states, 'logistics enum') + task = next((t for t in runtime['staff']['tasks'] + if t['target_entity_id'] == request['request_id'] + and t['task_type'] in ('supply_pickup', 'supply_delivery', 'supply_return') + and t['state'] not in ('COMPLETED', 'CANCELLED', 'FAILED')), None) + carrier = request['assigned_carrier_id'] + row = dict(source_facility_id=owner(request['source_inventory_id']), + destination_facility_id=owner(request['destination_inventory_id']), + carrier_name=candidates.get(carrier, {}).get('display_name', carrier) if carrier in staff else '', + reason_codes=[request['state_reason']] if request['state_reason'] else [], + request_id=request['request_id'], state=request['state'], reason=request['state_reason'], + carrier_id=carrier, source_inventory_id=request['source_inventory_id'], + destination_inventory_id=request['destination_inventory_id'], + item_requirements=request['item_requirements'], weight=request['total_weight'], + retry_count=request['retry_count'], task_id=task['task_id'] if task else '') + rows.append(row) + if request['state'] in ('COMPLETED', 'CANCELLED'): + recent.append((request['state_changed_at'], request['request_id'], row)) + else: + active.append(row) + recent.sort(key=lambda r: (r[0], r[1]), reverse=True) + close(report['goods'], dict(requests=rows, deliveries=active + [r[2] for r in recent[:10]]), 'goods') + + +def compare_badges(state, report): + if any('anchor' in b for b in report.get('badges', [])): + validate_observation_labels(report) + report = observation_anchor_report(report) + runtime = state['systems']['runtime'] + transform = Transform(runtime, report) + expected = [] + labels = dict(BROKEN='破損', NO_ACCESS='通路なし', NO_POWER='魔力なし', NO_STAFF='職員不足', + CLOSED='営業停止', DEGRADED='低下運転', OPEN='営業中') + for facility in runtime['facilities']['instances']: + fid = facility['instance_id'] + if facility['origin'][2] != report['layer']: continue + cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), []) + if not cells: continue + x,y,w,h = transform.room(cells) + room = report['rooms'][fid] + count, limit = room['count'], facility['customer_capacity'] + if facility['suspension_reason']: + business = '休止・摘発' if facility['suspension_reason'] == 'RAIDED' else '休止・破壊' + label = business + else: + business = labels.get(facility['state'], facility['state']) + if facility['state'] not in ('OPEN', 'DEGRADED'): + label = '未稼働' + else: + label = f'利用{count}/{limit}人' if limit >= 0 else f'利用{count}人' + if limit > 0 and count >= limit: label = '満員 ' + label + visible = len(room_slots([x,y,w,h], transform.dim[0]/16, facility['definition_id'])) + if count > visible: label += f'(他{count-visible}人)' + expected.append(dict(facility=fid, kind='business', at=[x+2,y+2], text=business+' '+label)) + requests = [r for r in report['goods']['requests'] if fid in (r['source_facility_id'], r['destination_facility_id'])] + if requests: + states = {} + for r in requests: states[r['state']] = states.get(r['state'], 0)+1 + text = '物資依頼 ' + ' / '.join(f'{k}:{states[k]}' for k in sorted(states)) + expected.append(dict(facility=fid, kind='supply', at=[x+2,y+16], requests=requests, text=text)) + close(report['badges'], expected, 'business/supply badges') + + +def compare_cabin_geometry(snapshot, report): + runtime = snapshot['systems']['runtime'] + transform = Transform(runtime, report) + expected = [] + p = transform.dim[0] / 16 + for shaft in elevator_report(snapshot)['shafts']: + if not shaft['stops'] or shaft['stops'][0][2] != report['layer']: + continue + floor = shaft['visual_floor'] + track = report['elevator_tracks'][shaft['shaft_id']] + close(track['floor'], floor, 'authoritative elevator track endpoint') + require(track['at'] <= report['animation_time'], 'elevator track time') + if not report['paused']: + # The prior-frame origin still needs a chained visual-history proof. + t = max(0, min(1, (report['animation_time'] - track['at']) / .42)) + floor = track['from'] + (floor - track['from']) * t + x, y, w, h = transform.rect(shaft['stops'][0]) + baseline = y + h - (floor - shaft['stops'][0][1]) * transform.dim[1] - 3 * p + def rounded(v): + return math.copysign(math.floor(abs(v) + .5), v) + rect = [rounded(x + 4*p), rounded(baseline - 19*p), max(shaft.get('width', 2), 1)*w - 8*p, 19*p] + if rect[0] >= report['viewport'][0] or rect[1] >= report['viewport'][1] or rect[0]+rect[2] <= 0 or rect[1]+rect[3] <= 0: + continue + count, capacity = len(shaft['boarded']), shaft['capacity'] + badge = f'{count}/{capacity}人' + if capacity > 0 and count >= capacity: + badge = '満員 ' + badge + if not shaft['operational']: + badge = '停止 ' + badge + else: + badge = '運転 ' + badge + doors = shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING') + badge += (' 開 ' if doors else ' 閉 ') + shaft['phase'] + expected.append(dict(shaft_id=shaft['shaft_id'], rect=rect, pixel=p, operational=shaft['operational'], + phase=shaft['phase'], passenger_count=count, doors_open=doors, + badge_rect=badge_rectangle([rect[0], rect[1]-14], badge), + badge_at=[rect[0], rect[1]-14], badge=badge)) + require(len(report['cabins']) == len(expected), 'cabin count') + for row, wanted in zip(report['cabins'], expected): + for key, value in wanted.items(): + close(row[key], value, 'cabin/' + key) + validate_cabin_intrusion(report) + + +def compare_motion(state, report, audit): + frames = audit['frames'] + require(len(frames) == 2, 'two motion history frames required') + previous, current = frames + require(type(previous['frame']) is int and current['frame'] > previous['frame'], 'motion frame order') + require(current['delta'] >= 0 and current['scaled_delta'] >= 0, 'motion delta') + before, after = previous['display'], current['display'] + for key, value in after.items(): + close(report[key], value, 'motion report binding/' + key) + close(after['animation_time'], before['animation_time'] + current['scaled_delta'], 'motion animation delta') + if before['paused'] and after['paused']: + close(current['scaled_delta'], 0, 'paused animation moved') + now = after['animation_time'] + runtime = state['systems']['runtime'] + cells = {'guest/' + r['guest_id']: r['current_cell'] for r in runtime['guests']['guests']} + cells.update({'staff/' + r['staff_id']: r['current_cell'] for r in runtime['staff']['staff']}) + def interpolated(track, endpoint, paused, transform=None): + t = 1 if paused else max(0, min(1, (now-track['at'])/.42)) + if transform: + a, b = transform.center(track['from']), transform.center(endpoint) + return [a[i] + (b[i]-a[i])*t for i in (0, 1)] + return track['from']+(endpoint-track['from'])*t + for key, track in after['actor_tracks'].items(): + require(key in cells and track['cell'] == cells[key], 'motion actor endpoint') + old = before['actor_tracks'].get(key) + if old is None: + wanted = dict(cell=cells[key], **{'from': cells[key]}, at=now, direction=1.0) + elif old['cell'] == cells[key]: + wanted = old + else: + a, b = old['cell'], cells[key] + origin = a if a[2] == b[2] and abs(a[0]-b[0])+abs(a[1]-b[1]) == 1 else b + wanted = dict(cell=b, **{'from': origin}, at=now, + direction=(1.0 if b[0] > a[0] else -1.0) if a[0] != b[0] else old['direction']) + close(track, wanted, 'actor interpolation origin/history') + # Ordinary walking centers; queue/room/rescue/working projections have + # independent semantic placement checks and do not use this center. + drawing = next((a for a in after['actor_drawings'] if a['key'] == key), None) + if drawing and drawing['mode'] in ('walk', 'carry_walk') and key not in report['queue_positions']: + position = interpolated(track, cells[key], after['paused'], Transform(runtime, report)) + # Same-cell crowd offsets are handled by the complete actor check. + require(all(math.isfinite(v) for v in position), 'nonfinite interpolation') + floors = {s['shaft_id']: s['visual_floor'] for s in elevator_report(state)['shafts']} + for key, track in after['elevator_tracks'].items(): + require(key in floors, 'unknown elevator motion') + old = before['elevator_tracks'].get(key) + if old is None: + wanted = dict(floor=floors[key], **{'from': floors[key]}, at=now) + elif math.isclose(old['floor'], floors[key], abs_tol=.00001, rel_tol=.00001): + wanted = old + else: + wanted = dict(floor=floors[key], **{'from': old['floor']}, at=now) + close(track, wanted, 'elevator interpolation origin/history') + if before['paused'] and after['paused'] and all(before[k] == after[k] for k in ('layer', 'zoom', 'pan', 'viewport', 'actor_tracks', 'elevator_tracks')): + prior_actors = {r['key']: r for r in before['actor_drawings']} + for row in after['actor_drawings']: + old = prior_actors.get(row['key']) + if old and all(old[k] == row[k] for k in ('state', 'mode', 'activity', 'receiving', 'carrying')): + close(row['at'], old['at'], 'paused actor moved') + return ['motion_history', 'interpolation', 'pause'] + + +def fitted_actor_layout(members, world_pixel, height): + """Independent interval solver. Inputs are reconstructed preferred rows/bounds.""" + def scaled(member, scale): + row = copy.deepcopy(member['row']) + row['scale'] = min(row['scale'], scale) + s = row['scale'] + row['at'] = [math.copysign(math.floor(abs(v)+.5), v) for v in + (row['center'][0]-8*s, row['center'][1]+height/2-3*world_pixel-17*s)] + return row + def pack(group, scale): + trial = [scaled(m, scale) for m in group] + right = -math.inf + for member, row in zip(group, trial): + x, y, w, h = actor_identity_core(row) + left = max(x, member['bounds'][0], right) + row['at'][0] += left-x + right = left+w + left = math.inf + for member, row in reversed(list(zip(group, trial))): + x, y, w, h = actor_identity_core(row) + bx, by, bw, bh = member['bounds'] + target = min(x, bx+bw-w, left-w) + row['at'][0] += target-x + left = target + if target < bx or y < by or y+h > by+bh: + return None + return trial + active = copy.deepcopy([m for m in members if not m['transit']]) + # A smaller sprite keeps its feet, so its vertical interval moves. Rebuild + # components after EVERY trial, including unsuccessful trials. Scale ceilings + # only decrease, making termination independent of an arbitrary iteration cap. + while True: + groups, bottom = [], -math.inf + for member in active: + member['row'] = scaled(member, member['row']['scale']) + for member in sorted(active, key=lambda m: (actor_identity_core(m['row'])[1], m['row']['key'])): + x, y, w, h = actor_identity_core(member['row']) + if y >= bottom: + groups.append([]) + bottom = y+h + else: + bottom = max(bottom, y+h) + groups[-1].append(member) + reduced = False + for group in groups: + group.sort(key=lambda m: (m['row']['center'][0], m['row']['key'])) + ceiling = max(1, *(m['row']['scale'] for m in group)) + if pack(group, ceiling) is None and ceiling > 1: + for member in group: + member['row'] = scaled(member, max(1, int(ceiling)-1)) + reduced = True + if not reduced: + break + result = {m['row']['key']: copy.deepcopy(m['row']) for m in members if m['transit']} + aggregations = [] + for group in groups: + group.sort(key=lambda m: (m['row']['center'][0], m['row']['key'])) + base = max(1, *(m['row']['scale'] for m in group)) + fitted = pack(group, base) + if fitted is None: + retained, fitted, identities = [], [], [] + for member in group: + row = member['row'] + identities.append(dict(key=row['key'], id=row['key'].split('/', 1)[1], + state=row['state'], cell=row['cell'], mode=row['mode'])) + attempt = pack(retained+[member], 1) + if attempt is not None: + retained.append(member) + fitted = attempt + aggregations.append(dict(members=identities, drawn_keys=[r['key'] for r in fitted], + at=group[0]['bounds'][:2], text='+'+str(len(group)-len(fitted)))) + result.update({row['key']: row for row in fitted}) + return result, aggregations + + +def actor_track_transit(report, key, cell): + """Track endpoint is bound to raw state by the caller, not to a draw flag.""" + require(type(report['paused']) is bool, 'actor pause must be boolean') + track = report['actor_tracks'][key] + close(track['cell'], cell, 'actor authoritative cell') + origin = track['from'] + require(isinstance(origin, list) and len(origin) == 3 + and all(type(v) is int for v in origin) + and origin[2] == cell[2] + and abs(origin[0]-cell[0])+abs(origin[1]-cell[1]) <= 1, + 'actor track is not a source-valid adjacent step') + now, started = report['animation_time'], track['at'] + require(all(type(v) in (int, float) and math.isfinite(v) for v in (now, started)) + and now >= started, 'invalid or future actor track time') + return not report['paused'] and origin != cell and now-started < .42 + + +def landing_actor_region(state, report, transform, cell, region, transit): + """Derive corridor landings from raw topology, never from actor flags.""" + if not any(intersects(region, cabin['rect']) for cabin in report['cabins']): + return dict(bounds=region, transit=transit) + runtime = state['systems']['runtime'] + footprint = {(stop[0]+dx, stop[1], stop[2]) + for shaft in elevator_report(state)['shafts'] for stop in shaft['stops'] + for dx in range(max(int(shaft.get('width', 2)), 1))} + walkable = {tuple(c) for c in runtime['world']['walkable_cells']} + facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id') + owners = {tuple(c): r['entity_id'] for r in runtime['world']['occupancies'] for c in r['cells']} + distance = lambda c: (abs(c[0]-cell[0]), c[0]) + origins = sorted((c for c in footprint if c[1:] == tuple(cell[1:])), key=distance) + lane = set() + for origin in origins: + for direction in (-1, 1): + for step in range(1, 9): + target = (origin[0]+direction*step, origin[1], origin[2]) + facility = facilities.get(owners.get(target)) + if (target in footprint or target not in walkable or + (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor'))): + break + lane.add(target) + lane = sorted(lane, key=distance) + if not lane: + rect = transform.rect(cell) + badges = [b for b in report['landing_badges'] if b['cell'][1:] == list(cell[1:])] + anchor = min(badges, key=lambda b: distance(b['cell']))['at'] if badges else [rect[0]-80, rect[1]-16] + return dict(bounds=region, transit=False, no_lane=True, badge_at=anchor) + reserved = list(report['queue_positions'].values()) + selected = next((c for c in lane if transform.center(c) not in reserved), lane[0]) + return dict(bounds=transform.rect(selected), transit=False, center=transform.center(selected)) + + +def compare_actor_geometry(state, report): + if any('anchor' in b for b in report.get('actor_aggregations', [])): + validate_observation_labels(report) + report = observation_anchor_report(report) + runtime = state['systems']['runtime'] + compare_queues(state, report) + compare_room_services(state, report) + boarded = {actor.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1): True + for shaft in elevator_report(state)['shafts'] for actor in shaft['boarded']} + close(report['boarded'], boarded, 'boarded actor coverage') + transform = Transform(runtime, report) + p, height = transform.dim[0]/16, transform.dim[1] + facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id') + staff = codec.indexed(runtime['staff']['staff'], 'staff_id') + occupancy = {r['entity_id']: r['cells'] for r in runtime['world']['occupancies']} + # Cabin rectangles and badges are independently recomputed before use below. + if report['cabins']: + compare_cabin_geometry(state, report) + counts, expected = {}, [] + def track_position(key, cell): + track = report['actor_tracks'][key] + actor_track_transit(report, key, cell) + t = 1 if report['paused'] else max(0, min(1, (report['animation_time']-track['at'])/.42)) + a, b = transform.center(track['from']), transform.center(cell) + return [a[i]+(b[i]-a[i])*t for i in (0, 1)] + def offset(cell, center): + key = tuple(cell) + n = counts.get(key, 0) + counts[key] = n+1 + return [center[0]+[0,4,-4,2,-2,6,-6][n%7]*p, center[1]-(n%2)*p] + def slot_position(fid, identifier, definition, fallback, dy, missing_fallback=False): + cells = occupancy.get(fid, []) + if not cells: + return fallback + users = report['rooms'].get(fid, {}).get('people', []) + if identifier not in users: + return fallback if missing_fallback else None + anchors = room_slots(transform.room(cells), p, definition) + index = users.index(identifier) + return [anchors[index][0], anchors[index][1]+dy] if index < len(anchors) else None + people = {'guest/'+r['guest_id']: r for r in runtime['guests']['guests']} + people.update({'staff/'+sid: r for sid, r in staff.items()}) + regions = {} + def actor_region(key): + if key in regions: + return regions[key] + person = people[key] + cell = person['current_cell'] + rect = transform.rect(cell) + transit = actor_track_transit(report, key, cell) + if transit: + other = transform.rect(report['actor_tracks'][key]['from']) + x, y = min(rect[0], other[0]), min(rect[1], other[1]) + rect = [x, y, max(rect[0]+rect[2], other[0]+other[2])-x, + max(rect[1]+rect[3], other[1]+other[3])-y] + result = dict(bounds=rect, transit=transit) + fid = '' + if key in report['queue_positions']: + cx, cy = report['queue_positions'][key] + result = dict(bounds=[cx-transform.dim[0]/2, cy-height/2, transform.dim[0], height], transit=False) + elif key.startswith('guest/'): + target = person['target_facility_id'] + if person['current_service_id'] and person['guest_id'] in report['rooms'].get(target, {}).get('people', []): + fid = target + else: + sid = person['staff_id'] + if sid in report['carried_patients']: + result = copy.deepcopy(actor_region('staff/'+report['carried_patients'][sid])) + result['bounds'][1] -= 12*p + if 'center' in result: + result['center'][1] -= 12*p + elif sid in report['resting_staff']: + fid = report['resting_staff'][sid] + elif sid in report['treated_patients']: + fid = report['treated_patients'][sid] + else: + target = person['assigned_facility_id'] + cells = occupancy.get(target, []) + workers = report['rooms'].get(target, {}).get('staff', []) + if person['state'] in ('WORKING', 'AVAILABLE') and cell in cells and sid in workers: + rect = transform.room(cells) + columns = max(int(rect[2]/(16*p)), 1) + slot = workers.index(sid) + if rect[3] >= 40*p and slot < columns: + result = dict(bounds=[rect[0]+slot*rect[2]/columns, rect[1], rect[2]/columns, rect[3]], transit=False) + if fid and occupancy.get(fid): + result = dict(bounds=transform.room(occupancy[fid]), transit=False) + if key not in report['queue_positions'] and 'center' not in result and not result.get('no_lane'): + result = landing_actor_region(state, report, transform, cell, result['bounds'], result['transit']) + regions[key] = result + return result + for guest in runtime['guests']['guests']: + key, cell = 'guest/'+guest['guest_id'], guest['current_cell'] + if key in report['boarded'] or key in report['queue_hidden'] or cell[2] != report['layer']: + continue + center = actor_region(key).get('center', track_position(key, cell))[:] + if key in report['queue_positions']: + center = report['queue_positions'][key] + elif guest['current_service_id']: + fid = guest['target_facility_id'] + definition = facilities.get(fid, {}).get('definition_id', '') + cells = occupancy.get(fid, []) + if cells and cells[0][2] == report['layer']: + center = slot_position(fid, guest['guest_id'], definition, center, + 3*p-height*.5-(5*p if definition in ('standard_room', 'premium_room') else 0), True) + else: + center = offset(cell, center) + if center is not None: + expected.append((key, center, False, bool(guest['current_service_id']), False, False)) + for sid, member in staff.items(): + key, cell = 'staff/'+sid, member['current_cell'] + if key in report['boarded'] or key in report['queue_hidden'] or cell[2] != report['layer']: + continue + center = actor_region(key).get('center', track_position(key, cell))[:] + special = True + if key in report['queue_positions']: + special = False + center = report['queue_positions'][key] + elif sid in report['carried_patients']: + carrier = staff.get(report['carried_patients'][sid]) + if carrier: + center = track_position('staff/'+carrier['staff_id'], carrier['current_cell']) + center[1] -= 12*p + center = actor_region(key).get('center', center)[:] + elif sid in report['resting_staff']: + center = slot_position(report['resting_staff'][sid], sid, 'staff_waiting_room', center, -height*.5) + elif sid in report['treated_patients']: + center = slot_position(report['treated_patients'][sid], sid, 'infirmary', center, -2*p-height*.5) + else: + special = False + center = offset(cell, center) + fid = member['assigned_facility_id'] + cells = occupancy.get(fid, []) + workers = report['rooms'].get(fid, {}).get('staff', []) + if member['state'] in ('WORKING', 'AVAILABLE') and fid in facilities and cell in cells and sid in workers: + rect = transform.room(cells) + columns = max(int(rect[2]/(16*p)), 1) + index = workers.index(sid) + if rect[3] >= 40*p and index < columns: + center = [rect[0]+(index+.5)*rect[2]/columns, rect[1]+26*p-height*.5] + center = report['queue_positions'].get(key, center) + if center is not None: + expected.append((key, center, True, False, bool(member['carried_inventory_id']) and not special, False)) + floor_keys = [r[0] for r in expected] + cabin_bounds = {} + shafts = {s['shaft_id']: s for s in elevator_report(state)['shafts']} + for cabin in report['cabins']: + shaft = shafts[cabin['shaft_id']] + x,y,w,h = cabin['rect'] + columns = max(int((w/p-4)/8), 1) + passengers = shaft['boarded'] + for i, actor_id in enumerate(passengers): + key = actor_id.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1) + is_staff = key.startswith('staff/') + center = [x+(2+(i%columns+.5)*8)*p, y+h-(2+((i//columns)%2)*9)*p+3*p-height*.5] + carried = bool(staff.get(key.removeprefix('staff/'), {}).get('carried_inventory_id')) if is_staff else False + expected.append((key, center, is_staff, False, carried, True)) + cabin_bounds[key] = cabin['rect'] + members, stranded = [], [] + for key, center, is_staff, receiving, carrying, riding in expected: + person = people[key] + cell = person['current_cell'] + track = report['actor_tracks'].get(key, {}) + relocation = {} if riding else actor_region(key) + if riding: + region, transit = cabin_bounds[key], False + else: + region, transit = relocation['bounds'], relocation['transit'] + scale = .5 if riding else .75 + scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale + at = [center[0]-8*scale, center[1]+height*.5-3*p-17*scale] + rounded = [math.copysign(math.floor(abs(v)+.5), v) for v in at] + hashed = 5381 + for char in key: + hashed = (hashed*33+ord(char)) & 0xffffffff + variant = hashed % 15 + frame = int(report['animation_time']*12)+variant + activity = facilities.get(person.get('target_facility_id'), {}).get('definition_id', '') if not is_staff else 'work' + if is_staff and person['role_id'] == 'cleaner': + activity = 'idle' + task = next((t for t in runtime['staff']['tasks'] if t['task_id'] == person['current_task_id']), None) + room = facilities.get(person['assigned_facility_id'], {}) + if (person['state'] == 'WORKING' and person['health'] > 0 and not person['is_retiring'] + and room.get('definition_id') == 'cleaning_room' and room.get('state') in ('OPEN', 'DEGRADED') + and not room.get('suspension_reason') and task and task['task_type'] == 'facility_clean' + and task['state'] == 'WORKING' and task['assigned_staff_id'] == person['staff_id'] + and cell == task['target_cell'] and any(r['cell'] == cell for r in runtime['path']['cell_traffic']) + and not any(t['actor_id'] == 'staff:'+person['staff_id'] for t in runtime['elevators']['trips']) + and task['target_entity_id'] in facilities + and any(r['id'] == task['target_entity_id'] for r in runtime['facility_cleanliness']['facilities'])): + activity = 'clean' + moving = (not receiving and key not in report['queue_positions'] and not riding + and not report['paused'] and track.get('cell') != track.get('from') + and report['animation_time']-track.get('at', 0) < .42) + mode = 'idle' + if is_staff and person['state'] == 'INCAPACITATED': + mode = 'collapsed' + elif receiving and activity in ('standard_room', 'premium_room'): + mode = 'standard_room_sleep' if activity == 'standard_room' else 'sleep' + elif moving: + mode = 'carry_walk' if carrying else 'walk' + elif carrying: + mode = 'carry' + elif is_staff and person['state'] == 'WORKING' and not riding: + mode = activity if activity in ('clean', 'idle', 'work') else 'work' + elif receiving and activity in ('cafeteria', 'tavern'): + mode = 'eat' + elif key in report['queue_positions'] or (not is_staff and person['state'] == 'QUEUING'): + mode = 'wait' + states = enum_names(ROOT, 'scripts/staff/staff_runtime.gd' if is_staff else 'scripts/guest/guest_runtime.gd', 'State') + row = dict(key=key, center=center, at=rounded, scale=scale, cell=cell, mode=mode, + frame=frame, variant=variant, flip=track.get('direction', 1) < 0, + staff=is_staff, state=states.index(person['state']), receiving=receiving, + carrying=carrying, riding=riding) + if relocation.get('no_lane'): + aggregate = next((a for a in stranded if a['at'] == relocation['badge_at']), None) + if aggregate is None: + aggregate = dict(members=[], drawn_keys=[], at=relocation['badge_at'], text='') + stranded.append(aggregate) + aggregate['members'].append(dict(key=key, id=key.split('/', 1)[1], state=row['state'], cell=cell, mode=mode)) + aggregate['text'] = '+'+str(len(aggregate['members'])) + else: + members.append(dict(row=row, bounds=[region[0]+p, region[1]+p, region[2]-2*p, region[3]-2*p], transit=transit)) + fitted, aggregations = fitted_actor_layout(members, p, height) + aggregations = stranded + aggregations + close(report.get('actor_aggregations'), aggregations, 'actor aggregation/no-fit') + require([r['key'] for r in report['actor_drawings']] == [k for k in floor_keys if k in fitted], 'actor drawing coverage/order') + actual = list(report['actor_drawings']) + [r for c in report['cabins'] for r in c['passengers']] + require(len(actual) == len(fitted) and {r['key'] for r in actual} == set(fitted), 'missing or duplicate actor') + for row in actual: + require('region' not in row and 'boarded' not in row, 'unsupported actor projection flags') + for field, value in fitted[row['key']].items(): + close(row[field], value, 'actor fitted '+field) + exemptions = {m['row']['key'] for m in members if m['transit']} + validate_actor_nonoverlap(report, transit_keys=exemptions) + return ['actor_geometry', 'passenger_geometry'] + + +def compare_layouts(a, b): + require(a['declaration'] == b['declaration'], 'identical predeclared inputs') + declaration = a['declaration'] + require(declaration['metric'] == 'sum_destination_arrival_minutes_entire_cohort', 'predeclared metric') + require(a['initial'] == b['initial'], 'identical seed budget clock definitions and initial state') + arrivals = {r['id']: r['offset'] for r in declaration['arrivals']} + require(len(arrivals) == len(declaration['arrivals']) and arrivals, 'whole nonempty cohort') + builds = [] + metrics = [] + for records in (a, b): + commands = [records[k] for k in sorted(records) if k.startswith('build-')] + require(all(r['result']['code'] == 'OK' for r in commands), 'paid construction succeeded') + builds.append([r['payload'] for r in commands]) + require({k.removeprefix('arrival-') for k in records if k.startswith('arrival-')} == set(arrivals), 'arrival cohort identity') + for gid, offset in arrivals.items(): + row = records['arrival-' + gid] + require(row['offset'] == offset and row['result']['code'] == 'OK', 'actual accepted arrival') + require(row['payload'] == a['arrival-' + gid]['payload'], 'same actual arrival command') + completed = {} + first_minute = records['tick-000']['minute'] + for offset in range(declaration['window'] + 1): + tick = records['tick-%03d' % offset] + require(tick['minute'] == first_minute + offset, 'contiguous actual clock') + for guest in tick['guests']['guests']: + gid = guest['guest_id'] + require(gid in arrivals and offset >= arrivals[gid], 'actual cohort membership') + if guest['current_cell'] == declaration['to']: + completed.setdefault(gid, offset - arrivals[gid]) + require(set(completed) == set(arrivals), 'no unfinished or omitted journeys') + metrics.append(sum(completed.values())) + require(builds[1] == builds[0] + [declaration['improvement']], 'only predeclared layout improvement') + require(b['tick-000']['minute'] == a['tick-000']['minute'], 'same observation start') + require(metrics[1] < metrics[0], 'strict whole-cohort improvement') + return dict(metric=declaration['metric'], A=metrics[0], B=metrics[1]) + + +def drawing_context(stream, capture, report): + """Shared OS/offline binding: a missing stream never grants exemptions.""" + require(isinstance(capture, dict) and all(k in capture for k in ('report', 'state', 'rest_audit')), + 'missing raw drawing context') + close(read_object(stream, capture['report']), report, 'raw-state report binding') + state = read_object(stream, capture['state']) + rest = read_object(stream, capture['rest_audit']) + require(isinstance(state, dict) and isinstance(state.get('systems', {}).get('runtime'), dict), + 'malformed raw state context') + require(isinstance(rest, dict) and isinstance(rest.get('staff'), list) + and 'path_service_sha256' in rest, 'malformed rest authority') + return state, rest + + +def read_object(directory, digest): + require(isinstance(digest, str) and re.fullmatch('[0-9a-f]{64}', digest), 'invalid object digest') + path = directory / 'objects' / (digest + '.json') + require(not path.is_symlink() and sha(path) == digest, 'whole typed object hash') + typed = json.loads(path.read_bytes()) + require(typed.get('type') == 'Dictionary', 'whole dictionary required') + return decode(typed) + + +def inspect_stream(directory, root=ROOT): + labels = set() + checked = [] + with (directory / 'manifest.jsonl').open() as stream: + for line in stream: + row = json.loads(line) + if 'object' in row: + read_object(directory, row['object']) + continue + require(row['label'] not in labels, 'duplicate streamed label') + labels.add(row['label']) + require(type(row['pid']) is int and row['pid'] > 0, 'actual diagnostic PID required') + state = read_object(directory, row['state']) + close(read_object(directory, row['state_after']), state, 'read-only audit snapshot identity') + report = read_object(directory, row['report']) + fields = compare_actor_rows(state, report, root) + require(report['elevators'] == elevator_report(state), 'typed elevator report mismatch') + validate_observation_labels(report) + compare_room_services(state, report, root) + compare_rest(state, report, read_object(directory, row['rest_audit']), root) + compare_queues(state, report) + compare_traffic(state, report) + compare_goods(state, report, root) + compare_badges(state, report) + compare_cabin_geometry(state, report) + compare_motion(state, report, read_object(directory, row['motion'])) + compare_actor_geometry(state, report) + checked.append(dict(label=row['label'], fields=fields + ['elevators', 'room_service_fields', + 'queues', 'traffic', 'goods', 'cabin_geometry'])) + require(checked, 'empty diagnostic stream') + return checked + + +def condition_matrix(executed=None): + executed = executed or {} + result = [] + for condition in CONDITIONS: + categories = {} + for category in CATEGORIES: + actual = set(executed.get(condition, {}).get(category, [])) + required = REQUIRED_CHECKS[condition][category] + missing = sorted(required - actual) + categories[category] = dict(checks=sorted(actual), missing=missing, + status='passed' if actual and not missing else 'pending') + result.append(dict(condition=condition, categories=categories, + status='passed' if all(c['status'] == 'passed' for c in categories.values()) else 'pending')) + return result + + +def BLOCKERS(executed=None): + return {r['condition']: {c: d['missing'] for c, d in r['categories'].items() if d['missing']} + for r in condition_matrix(executed) if r['status'] != 'passed'} + + +CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' +PLAN = 'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md' +BASE_COMMIT = '860136f7099f5e3aee99da5798a627a77f2d8eea' +ALLOWED_PATHS = ('scenes/main/world_preview.gd', 'scripts/ui/room_presentation.gd', + 'tests/ui/test_p23_life_observation.gd', 'tests/ui/test_p23_life_observation.gd.uid', + 'tests/fixtures/p23-03/**', 'tools/run_p23_life_observation.py', + 'tools/check_p23_life_observation_evidence.py', 'tools/test_p23_life_observation_evidence.py', + 'tools/regression/suite-policy.json', + 'docs/plans/original-completion-2026-10-02/execution/p23-03/**', CONTRACT) + + +def strict_json(data): + def pairs(items): + result = {} + for key, value in items: + require(key not in result, 'duplicate JSON key: ' + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=pairs, + parse_constant=lambda value: require(False, 'nonfinite JSON: ' + value)) + + +def marker_join(base, submitted, main): + """Preserve M verbatim structurally, adding exactly the S P23 row.""" + test = 'tests/ui/test_p23_life_observation.gd' + for document in (base, submitted, main): + rows = document['rows'] + require(len({row['test'] for row in rows}) == len(rows), 'duplicate marker test') + additions = [row for row in submitted['rows'] if row['test'] == test] + require(len(additions) == 1 and not any(row['test'] == test for row in base['rows'] + main['rows']), + 'marker must add exactly one new P23 row') + stripped = copy.deepcopy(submitted) + stripped['rows'] = [row for row in stripped['rows'] if row['test'] != test] + require(structural_equal(stripped, base), 'submitted marker changes non-P23 structure') + joined = copy.deepcopy(main) + joined['rows'].append(copy.deepcopy(additions[0])) + return joined + + +def structural_equal(a, b): + return json.dumps(a, sort_keys=True, ensure_ascii=False, allow_nan=False) == json.dumps( + b, sort_keys=True, ensure_ascii=False, allow_nan=False) + + +def verify_marker_join(base, submitted, main, tested): + expected = marker_join(base, submitted, main) + p23 = expected['rows'][-1] + require(structural_equal([row for row in tested['rows'] if row['test'] == p23['test']], [p23]), + 'joined marker P23 row changed or duplicated') + # merge-tree determines the insertion position; deleting the one addition + # must leave M's entire ordered structure, including all P18 rows, intact. + stripped = copy.deepcopy(tested) + stripped['rows'] = [row for row in stripped['rows'] if row['test'] != p23['test']] + require(structural_equal(stripped, main), 'completion marker is not the strict structural join') + + +def git_bytes(root, *arguments): + """Root-only adapter. Worker tests inject an in-memory object database.""" + process = subprocess.run(['git', '-C', str(root), *arguments], capture_output=True, check=False) + require(process.returncode == 0, 'Git composition failed/conflicted: ' + ' '.join(arguments)) + return process.stdout + + +_VERIFIED_VIEWS = {} + + +def blob_oid(content): + return hashlib.sha1(b'blob %d\0' % len(content) + content).hexdigest() + + +def git_blob_stream(root, oids, read_batch=None): + """Pace requests to one owned child; adapters return (binary stream, status).""" + requested = list(oids) + require(all(isinstance(oid, str) and re.fullmatch('[0-9a-f]{40}', oid) + for oid in requested), 'invalid blob oid') + process = stream = None + try: + if read_batch is None: + process = subprocess.Popen(['git', '-C', str(root), 'cat-file', '--batch'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + stream = process.stdout + else: + stream, status = read_batch(requested) + count = 0 + for oid in requested: + if process is not None: + process.stdin.write((oid + '\n').encode()) + process.stdin.flush() + header = stream.readline(128) + match = re.fullmatch(rb'([0-9a-f]{40}) blob (0|[1-9][0-9]*)\n', header) + require(match is not None and match[1].decode() == oid, 'invalid/out-of-order blob header') + size = int(match[2]) + content = stream.read(size) + require(len(content) == size, 'short blob read') + require(stream.read(1) == b'\n', 'missing blob LF') + require(blob_oid(content) == oid, 'blob content SHA-1 mismatch') + count += 1 + yield oid, content + if process is not None: + process.stdin.close() + require(stream.read(1) == b'', 'extra trailing blob output') + require((process.wait() if process is not None else status) == 0, 'cat-file failed') + require(count == len(requested), 'blob output count mismatch') + finally: + if process is not None: + if process.poll() is None: + process.kill() + process.wait() + process.stdin.close() + process.stdout.close() + elif stream is not None: + stream.close() + + +def verify_composition(root, roles, source_map, inputs, read_git=None, view=None, read_batch=None): + require(isinstance(roles, dict), 'missing externally bound S/M/T/Q') + names = ('submitted_sha', 'main_context_sha', 'tested_tree', 'tested_commit') + require(all(isinstance(roles.get(k), str) and re.fullmatch('[0-9a-f]{40}', roles[k]) for k in names), + 'invalid externally bound S/M/T/Q') + s, m, t, q = (roles[k] for k in names) + git = read_git or (lambda *args: git_bytes(root, *args)) + for commit in (BASE_COMMIT, s, m, q): + require(git('cat-file', '-t', commit).strip() == b'commit', 'role is not a commit') + require(git('cat-file', '-t', t).strip() == b'tree', 'T is not a tree') + require(git('rev-parse', q+'^{tree}').decode().strip() == t, 'Q does not contain T') + head = git('rev-parse', 'HEAD').decode().strip() + if head not in (s, q): + require(git('rev-parse', 'HEAD^{tree}').decode().strip() == t, 'C tree differs from T') + require(git('rev-list', '--parents', '-n', '1', 'HEAD').decode().split() == [head, m, s], + 'C parents must be exactly [M,S]') + require(not git('status', '--porcelain'), 'tested checkout is dirty') + merged = git('merge-tree', '--write-tree', s, m).decode().splitlines() + require(merged and merged[0] == t, 'T differs from deterministic merge-tree') + def tree(revision): + result = {} + for record in git('ls-tree', '-r', '-z', revision).split(b'\0'): + if not record: + continue + metadata, path = record.split(b'\t', 1) + mode, kind, oid = metadata.decode().split() + require(path.decode() not in result, 'duplicate T path') + result[path.decode()] = (mode, kind, oid) + return result + base, submitted, main, tested = map(tree, (BASE_COMMIT, s, m, t)) + changed_s = {p for p in base.keys() | submitted.keys() if base.get(p) != submitted.get(p)} + changed_m = {p for p in base.keys() | main.keys() if base.get(p) != main.get(p)} + def allowed(path): + return any(path.startswith(rule[:-2]) if rule.endswith('/**') else path == rule + for rule in ALLOWED_PATHS) + require(all(allowed(p) for p in changed_s), 'S changed a path outside allowed_paths') + for paths, authority, label in ((changed_s-changed_m, submitted, 'S-only'), + (changed_m-changed_s, main, 'M-only')): + for path in paths: + require(tested.get(path) == authority.get(path), label + ' blob/mode provenance: ' + path) + def document(revision): + return strict_json(git('show', revision + ':' + CONTRACT)) + verify_marker_join(document(BASE_COMMIT), document(s), document(m), document(t)) + runner = module('p23_composition_source_filter', ROOT/'tools/run_all_tests.py') + if view is not None: + registered = _VERIFIED_VIEWS.get(Path(view).resolve()) + require(registered is not None, 'unregistered verified view') + require(registered[0] == t, 'verified view tree mismatch') + require(registered[1] == {p: e for p, e in tested.items() if e[1] == 'blob'}, + 'verified view manifest mismatch') + with (contextlib.nullcontext(Path(view)) if view is not None else + tested_checkout(root, roles, git, read_batch)) as source_view: + require(source_map == {p: row['sha256'] for p, row in runner.source_refs(source_view).items()}, + 'incomplete tested-tree source/settings/QA map') + # Source refs include all QA suites and settings, not just S-changed files. + required = set(source_map) | {p.split('#', 1)[0] for p in inputs} | {PLAN, CONTRACT, + 'project.godot', 'tests/business/test_required_kitchen.gd', 'tools/regression/suite-policy.json'} + bindings = roles.get('blob_bindings') + require(isinstance(bindings, dict) and set(bindings) == required, 'incomplete/extraneous T blob bindings') + paths = sorted(required) + for path in paths: + entry = tested.get(path) + require(entry is not None and entry[1] == 'blob' and bindings[path] == entry[2], 'T blob binding: ' + path) + contents = (((tested[p][2], (Path(view)/p).read_bytes()) for p in paths) if view is not None + else git_blob_stream(root, [tested[p][2] for p in paths], read_batch)) + with contextlib.closing(contents): + for number, (oid, content) in enumerate(contents): + path = paths[number] + require(blob_oid(content) == tested[path][2] == oid, 'binding blob SHA-1 mismatch: ' + path) + if head != s or path in changed_s-changed_m: + require((root/path).read_bytes() == content, 'checkout bytes differ from T: ' + path) + digest = hashlib.sha256(content).hexdigest() + if path in source_map: + require(source_map[path] == digest, 'evidence source is not T: ' + path) + if path in inputs: + require(inputs[path] == digest, 'evidence input is not T: ' + path) + return {k: roles[k] for k in names} + + +@contextlib.contextmanager +def tested_checkout(root, roles, read_git=None, read_batch=None): + """Disposable Git-blob view shared by the existing filters and readers.""" + require(isinstance(roles, dict), 'missing externally bound S/M/T/Q') + tree = roles.get('tested_tree', '') + require(isinstance(tree, str) and re.fullmatch('[0-9a-f]{40}', tree), 'invalid T') + git = read_git or (lambda *args: git_bytes(root, *args)) + with tempfile.TemporaryDirectory(prefix='p23-tested-tree-') as temporary: + view = Path(temporary) + manifest = {} + for record in git('ls-tree', '-r', '-z', tree).split(b'\0'): + if not record: + continue + metadata, raw_path = record.split(b'\t', 1) + mode, kind, oid = metadata.decode().split() + path = Path(raw_path.decode()) + require(not path.is_absolute() and '..' not in path.parts, 'unsafe T path') + require(kind == 'blob' and mode in ('100644', '100755'), 'T source aliases refused') + require(path.as_posix() not in manifest, 'duplicate T path') + manifest[path.as_posix()] = (mode, kind, oid) + paths = list(manifest) + contents = git_blob_stream(root, [manifest[p][2] for p in paths], read_batch) + with contextlib.closing(contents): + for number, (_, content) in enumerate(contents): + target = view/paths[number] + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + _VERIFIED_VIEWS[view.resolve()] = (tree, manifest) + try: + yield view + finally: + del _VERIFIED_VIEWS[view.resolve()] + + +def root_context(directory, index, explicit=None): + path = directory/'root-composition.json' + require(path.is_file() and not path.is_symlink() + and path.resolve().is_relative_to(directory.resolve()), 'missing/unsafe Root composition context') + data = path.read_bytes() + require(index.get('root_composition_sha256') == hashlib.sha256(data).hexdigest(), + 'Root composition context SHA mismatch') + roles = strict_json(data) + require(explicit is None or structural_equal(explicit, roles), 'explicit Root context disagrees') + return roles + + +def normative_plan_text(root): + """Requirement text is an input; explicitly labelled progress is metadata.""" + text = (root / PLAN).read_text() + section = re.search(r'^### P23[^\n]*\n(.*?)(?=^### |\Z)', text, re.M | re.S) + require(section is not None, 'missing normative #p23 plan section') + lines = [] + for line in section.group(0).splitlines(): + if line.startswith('現在地:'): + continue + line = line.split(' 状態:', 1)[0].rstrip() + if line: + lines.append(line) + require(any(line.startswith('- P23-03 ') and '受入:' in line for line in lines), + 'missing P23-03 work/acceptance plan text') + return '\n'.join(lines) + '\n' + + +def input_map(root): + names = [SPEC, CONTRACT, 'AGENTS.md', 'docs/traceability/original/BASELINE.txt'] + names += [p.relative_to(root).as_posix() for p in (root / 'tests/fixtures/p23-03').rglob('*') + if p.is_file()] + pins = {name: sha(root / name) for name in sorted(names)} + pins[PLAN + '#p23-normative'] = hashlib.sha256(normative_plan_text(root).encode()).hexdigest() + return pins + + +def input_provenance(root): + # Full-plan progress changes deliberately do not participate in input equality. + return {'full_plan_sha256': sha(root / PLAN)} + + +def inspect_os_receipt(receipt, source_map, pins=None): + require(receipt.get('engine_ended') is True, 'engine_ended must be true') + require(receipt.get('passed') is True and receipt.get('exit_code') == 0, 'unsuccessful OS process') + require(not receipt.get('error') and receipt.get('guard_errors') == [] + and receipt.get('cleanup_errors') == [], 'OS cleanup/guard failure') + require(receipt.get('default_before') == receipt.get('default_after') + and isinstance(receipt.get('default_before'), dict), 'default profile changed') + require(receipt.get('stage_before') == receipt.get('stage_after') + and isinstance(receipt.get('stage_before'), dict), 'stage changed') + require(receipt.get('source_map') == source_map, 'OS source pin differs') + after = receipt.get('source_after', {}) + require({name: pin['sha256'] for name, pin in after.items()} == source_map, 'source changed') + cleanup = receipt.get('owned_cleanup') + require(isinstance(cleanup, list) and cleanup + and all(row.get('engine_ended') is True for row in cleanup), 'missing owned cleanup') + require(bool(receipt.get('ordinary_owner_proof')), 'missing ordinary audio owner proof') + for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'): + require(isinstance(receipt.get(field), str) and re.fullmatch('[0-9a-f]{64}', receipt[field]) + and receipt[field] == receipt.get(field + '_after'), 'OS pin changed: ' + field) + require(receipt.get('spec_sha256') == SPEC_SHA, 'OS immutable spec') + require(receipt.get('inputs') and receipt['inputs'] == receipt.get('inputs_after'), 'OS inputs changed') + if pins is not None: + require(receipt['inputs'] == pins, 'OS input applicability') + require(receipt.get('environment') and receipt['environment'] == receipt.get('environment_after'), 'OS environment changed') + require(type(receipt.get('actual_pid')) is int and receipt['actual_pid'] > 0, 'OS actual PID') + require(receipt.get('profile') and receipt.get('profile_name') and isinstance(receipt.get('profile_after'), dict), 'OS private profile provenance') + before = receipt.get('profile_before', {}) + require(type(before.get('existed')) is bool and isinstance(before.get('files'), dict), 'OS profile before') + for member, pin in before['files'].items(): + require(receipt['profile_after'].get(member) == pin, 'OS profile input changed: ' + member) + + +def inspect_resume(writer, reader, saved, requests, formal_members): + require(type(writer['actual_pid']) is int and type(reader['actual_pid']) is int + and writer['actual_pid'] > 0 and reader['actual_pid'] > 0 + and writer['actual_pid'] != reader['actual_pid'], 'resume requires distinct actual PIDs') + require(saved['pid'] == writer['actual_pid'], 'formal writer PID') + require(set(formal_members) == {'world.bin', 'manifest.json', 'thumbnail.png', 'journal.log'}, 'formal members coverage') + close(saved['members'], formal_members, 'formal member hashes') + require({k: v['sha256'] for k, v in reader['formal_input'].items()} == formal_members, 'reader formal input hashes') + loads = [row for row in requests if row['request']['action'] == 'load_menu'] + require(len(loads) == 1, 'missing or duplicate normal load ACK') + item = loads[0] + request, ack, observed, cua = [item[k] for k in ('request', 'ack', 'observed', 'cua')] + require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == reader['actual_pid'], 'load ACK PID') + require(request['sequence'] == ack['sequence'] == observed['sequence'], 'load ACK request ID') + require(ack['request_sha256'] == item['request_sha256'] == observed['request_sha256'], 'load ACK hash') + require(0 < request['requested_ticks_usec'] < observed['ack_observed_ticks_usec'], 'load ACK time order') + require(cua['method'] == 'native_os' and cua['tool_call_id'] and cua['timestamp_ns'] > 0, 'load CUA provenance') + require(request['requested_unix_ns'] <= cua['timestamp_ns'] <= ack['timestamp_ns'] <= observed['ack_observed_unix_ns'], 'load external ACK time order') + require(observed['status'] == '手動セーブを読み込みました。', 'normal load result') + return ['distinct_pid', 'formal_members', 'load_ack'] + + +def require_screenshot_coverage(action, screenshots): + require(isinstance(screenshots, list) and len(screenshots) >= (1 if action == 'screenshot' else 2), + 'screenshot-only needs one image; real input needs before/after images') + + +def inspect_os_requests(directory, layout, role, pid, root=ROOT): + pair = directory/'os-pair'/layout + def path(name): + result = directory/name + require(result.resolve().is_relative_to(directory.resolve()) and not result.is_symlink(), 'OS artifact escape') + return result + def document(name): + return json.loads(path(name).read_bytes()) + listing = json.loads((pair/(role+'-requests.json')).read_bytes()) + require(listing['pid'] == pid and listing['requests'] and len(set(listing['requests'])) == len(listing['requests']), 'OS request list') + bindings = json.loads((pair/(role+'-screenshots.json')).read_bytes()) + seen, records, pixel_categories = set(), [], set() + stream = pair/(role+'-stream') + captures = {row['label']: row for row in map(json.loads, (stream/'manifest.jsonl').read_text().splitlines()) if 'report' in row} + prior_tick = -1 + for sequence, ack_name in enumerate(listing['requests'], 1): + ack_path = path(ack_name) + request_path = ack_path.with_name('request.json') + observed = json.loads(ack_path.with_name('observed.json').read_bytes()) + request, ack = json.loads(request_path.read_bytes()), json.loads(ack_path.read_bytes()) + cua = document(ack['input_receipt']) + require_screenshot_coverage(request['action'], cua.get('screenshots')) + require(request['sequence'] == ack['sequence'] == observed['sequence'] == sequence, 'OS request sequence') + require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == pid, 'OS request PID') + digest = sha(request_path) + require(ack['request_sha256'] == observed['request_sha256'] == digest, 'OS ACK hash') + require(prior_tick < request['requested_ticks_usec'] < observed['ack_observed_ticks_usec'], 'OS ACK order') + prior_tick = observed['ack_observed_ticks_usec'] + require(request['requested_unix_ns'] <= cua['timestamp_ns'] <= ack['timestamp_ns'] <= observed['ack_observed_unix_ns'], 'OS external ACK time order') + require(cua['method'] == 'native_os' and cua['tool_name'] and cua['tool_call_id'] + and cua['timestamp_ns'] > 0 and cua['collector_pid'] > 0 and cua['collector_pid'] != pid, 'OS CUA provenance') + require('arguments' in cua and isinstance(cua['arguments'], dict), 'full CUA arguments required') + if request['action'] != 'screenshot': + require(cua['events'] and observed['events'], 'OS input events missing') + if request['action'] == 'wheel': + require(request['ctrl'] is False and any(e.get('ctrl') is False and e.get('pressed') for e in observed['events']), 'ordinary wheel evidence') + report = decode(request['report']) + capture = captures[request_path.parent.name] + state, rest_audit = drawing_context(stream, capture, report) + for number, shot in enumerate(cua['screenshots']): + matches = [b for b in bindings if b['request'] == str(request_path.relative_to(directory)) and b['screenshot_index'] == number] + require(len(matches) == 1, 'screenshot binding coverage') + binding = matches[0] + identity = (binding['request'], number) + require(identity not in seen, 'duplicate screenshot binding') + seen.add(identity) + require(binding['request_sha256'] == digest and binding['target_pid'] == pid and binding['sequence'] == sequence, + 'screenshot request attribution') + require(binding['input_receipt'] == ack['input_receipt'] and binding['input_receipt_sha256'] == sha(path(ack['input_receipt'])), 'screenshot CUA receipt hash') + require(binding['raw_path'] == shot['path'] and binding['raw_sha256'] == shot['raw_sha256'] == sha(path(shot['path'])) + and binding['tool_call_id'] == shot['tool_call_id'] and shot['tool_call_id'], 'screenshot raw provenance') + conversion = document(binding['conversion']) if binding['conversion'] else None + image = screenshot_pixels(path(shot['path']), conversion, shot) + require(binding['png_sha256'] == sha(path(binding['png_path'])) and (binding['width'], binding['height']) == image[:2], 'decoded screenshot binding') + if conversion: + require(Path(conversion['png_path']).resolve() == path(binding['png_path']).resolve(), 'converted output binding') + for suffix in ('stdout', 'stderr'): + require(sha(path(binding['conversion']+'.'+suffix)) == conversion[suffix+'_sha256'], 'converter log hash') + else: + require(binding['png_path'] == binding['raw_path'], 'PNG must bind original bytes') + # Screenshot actions observe a stable report; input before/after + # screenshots are provenance only and never relabelled as this frame. + if request['action'] == 'screenshot': + clicks = [document(json.loads(path(name).read_bytes())['input_receipt']) + for name in listing['requests']] + frame = frame_rows(report, decode(request['rectangles']), request, cua, [image], clicks, root, + state, rest_audit) + review_dir = request_path.parent/('review-image-' + str(number)) + manifest, manifest_sha = write_frame_review(review_dir, frame, [dict( + whole_image_sha256=binding['png_sha256'], raw_sha256=shot['raw_sha256'], + conversion_sha256=sha(path(binding['conversion'])) if conversion else None, + request_sha256=digest, receipt_sha256=sha(path(ack['input_receipt'])))]) + require(frame['mechanical_pass'], 'frame mechanical checks: ' + str(frame['errors']) + + str([r['failures'] for r in frame['rows'] if not r['mechanical_pass']])) + pixel_categories.update(reviewed_pixel_categories( + manifest, manifest_sha, review_dir/'independent-verdict.json', require_judgments=True)) + records.append(dict(request=request, ack=ack, observed=observed, cua=cua, request_sha256=digest)) + require(len(seen) == len(bindings), 'unattributed screenshot bindings') + return records, pixel_categories + + +def inspect_native_phases(directory, index, root=ROOT): + runner = module('p23_existing_runner_checks', root/'tools/run_all_tests.py') + expected = ['runs/'+name+'/'+name+'-receipt.json' for name in ('import', 'parse', 'layout-a', 'layout-b')] + require(index['phases'] == expected, 'native phase coverage/order') + for name in expected: + path = directory/name + receipt = json.loads(path.read_bytes()) + pins = json.loads(path.with_name('p23-inputs.json').read_bytes()) + require(pins['receipt_sha256'] == sha(path), 'native receipt pin') + require(pins['source_map'] == index['source_map'] and {k: v['sha256'] for k, v in pins['source_after'].items()} == index['source_map'], 'native sources changed') + require(pins['inputs'] == pins['inputs_after'] == input_map(root) and pins['spec_sha256'] == SPEC_SHA, 'native input applicability') + require(pins['default_before'] == pins['default_after'] and pins['stage_before'] == pins['stage_after'], 'native profile/stage changed') + require(pins['settings_sha256'] == sha(root/'project.godot'), 'native settings changed') + require(receipt['passed'] is True and receipt['failed'] is False and receipt['engine_ended'] is True + and receipt['exit_code'] == 0 and not receipt.get('validation_error') and not receipt.get('guard_error'), 'native process failed') + capture = receipt['outer_actual_capture'] + require(capture['passed'] is True and capture['all_captured_ended'] is True, 'native owned cleanup') + require(receipt['whole_startup_profile_after']['fresh_before_engine'] is True, 'native profile protection') + require(receipt['default_player_entire_profile_unchanged'] is True, 'native default profile changed') + require(isinstance(receipt['controlled_environment'], dict), 'native environment absent') + diagnostics = receipt['finite_original_diagnostic_review'] + require(runner.process_gate(receipt['native_process'], diagnostics['allowed_e31_error_lines']), 'native process gate') + if receipt['test']: + require(receipt['source_sha256'] == receipt['source_sha256_end'] == sha(root/receipt['test']) + and receipt['completion']['passed'] is True and receipt['reported_checks'] > 0 and receipt['reported_failures'] == 0, 'native strict marker') + + +def check(root, directory, root_roles=None): + try: + index_path = directory/'index.json' + if not index_path.is_file(): + return ['missing evidence index: ' + str(index_path)] + index = strict_json(index_path.read_bytes()) + roles = root_context(directory, index, root_roles) + with tested_checkout(root, roles) as view: + return check_tested(root, directory, roles, view) + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + return ['invalid evidence index: ' + str(exc)] + + +def check_tested(checkout, directory, root_roles, root): + """Never infer completion of unimplemented gates from an index assertion.""" + deficits = [] + executed = {c: {category: set() for category in CATEGORIES} for c in CONDITIONS} + semantic, pixel_categories, anchors = set(), set(), set() + index_path = directory / 'index.json' + if not index_path.is_file(): + return ['missing evidence index: ' + str(index_path)] + try: + index = json.loads(index_path.read_bytes()) + require(index.get('schema') == 'p23-life-observation-v2', + 'historical v1 evidence has diagnostic applicability only') + runner = module('p23_source_applicability', root/'tools/run_all_tests.py') + require(index.get('source_map') == {n: v['sha256'] for n, v in runner.source_refs(root).items()}, 'exact frozen source map') + roles = verify_composition(checkout, root_roles, index['source_map'], input_map(root), view=root) + if git_bytes(checkout, 'rev-parse', 'HEAD').decode().strip() != roles['submitted_sha']: + require(index['source_map'] == {n: v['sha256'] for n, v in runner.source_refs(checkout).items()}, + 'exact checkout source map') + require(index.get('composition') == roles and index.get('commit') == roles['tested_commit'], + 'evidence role binding differs from Root S/M/T/Q; historical v1 is not T evidence') + if index.get('input_map') != input_map(root): + deficits.append('normative input_map differs from current immutable inputs') + if index.get('spec_sha256') != SPEC_SHA: + deficits.append('immutable spec SHA missing or different') + if index.get('marker_contract_sha256') != sha(root / CONTRACT): + deficits.append('marker contract SHA missing or different') + artifacts = index.get('artifacts', {}) + if not artifacts: + deficits.append('missing indexed evidence artifacts') + for name, digest in artifacts.items(): + path = directory / name + require(path.resolve().is_relative_to(directory.resolve()) and not path.is_symlink(), + 'artifact escapes evidence directory') + require(path.is_file() and sha(path) == digest, 'artifact SHA mismatch: ' + name) + try: + inspect_native_phases(directory, index, root) + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + deficits.append('native evidence deficit: ' + str(exc)) + for layout in ('A', 'B'): + layout_semantic, layout_pixels = set(), set() + stream = directory / ('layout-' + layout) + if (stream / 'manifest.jsonl').is_file(): + inspect_stream(stream, root) + else: + deficits.append('missing native layout-' + layout + ' typed diagnostic stream') + for role in ('writer', 'reader'): + stream = directory / 'os-pair' / layout / (role + '-stream') + if (stream / 'manifest.jsonl').is_file(): + inspect_stream(stream, root) + # Successful independent recalculation, with nonempty + # observed rows required before a category earns coverage. + for line in (stream/'manifest.jsonl').read_text().splitlines(): + capture = json.loads(line) + if 'report' not in capture: continue + report = read_object(stream, capture['report']) + presence = {'room': report['rooms'], 'queue': report['landing_badges'], + 'EV': report['cabins'], 'staff': report['staff'], + 'goods': report['goods']['requests'], 'business': report['badges']} + semantic.update('semantic_'+c for c, rows in presence.items() if rows) + layout_semantic.update(c for c, rows in presence.items() if rows) + if any(r['people'] for r in report['rooms'].values()): anchors.add('room_users') + if any(r['overflow'] > 0 for r in report['landing_badges']): anchors.add('queue_overflow') + if any(r['passengers'] for r in report['cabins']): anchors.add('elevator_passengers') + if report['staff'] and report['tasks']: anchors.add('staff_jobs') + if any(r['carried_inventory_id'] for r in report['staff']) and report['goods']['requests']: anchors.add('carried_goods') + if role == 'reader': anchors.add('resume_recalculation') + else: + deficits.append('missing OS ' + layout + '-' + role + ' typed diagnostic stream') + ended = directory / 'os-runs' / (layout + '-' + role) / 'ended.json' + if ended.is_file(): + receipt = json.loads(ended.read_bytes()) + inspect_os_receipt(receipt, index.get('source_map'), input_map(root)) + records, categories = inspect_os_requests(directory, layout, role, receipt['actual_pid'], root) + pixel_categories.update('pixels_'+c for c in categories if c in CATEGORIES) + layout_pixels.update(categories) + if role == 'reader': + writer = json.loads((directory/'os-runs'/(layout+'-writer')/'ended.json').read_bytes()) + pair = directory/'os-pair'/layout + members = {p.name: sha(p) for p in (pair/'formal').iterdir() if p.is_file()} + inspect_resume(writer, receipt, json.loads((pair/'save.json').read_bytes()), records, members) + require((pair/'writer-formal-model.var').read_bytes() == (pair/'reader-formal-model.var').read_bytes(), 'resumed whole formal model') + anchors.add('distinct_pid_resume') + else: + deficits.append('missing OS ' + layout + '-' + role + ' process receipt') + for category in CATEGORIES: + if category not in layout_semantic or category not in layout_pixels: + deficits.append('missing actual OS ' + layout + ' semantic/pixel coverage: ' + category) + if all((directory / ('layout-' + layout) / 'manifest.jsonl').is_file() for layout in ('A', 'B')): + compare_layouts(comparison_records(directory / 'layout-A'), comparison_records(directory / 'layout-B')) + anchors.add('route_improvement') + if not deficits: + anchors.add('os_guards') + for condition in CONDITIONS: + for category in CATEGORIES: + executed[condition][category].update((semantic | pixel_categories | anchors) & REQUIRED_CHECKS[condition][category]) + deficits.extend(condition + ': ' + json.dumps(missing, ensure_ascii=False) for condition, missing in BLOCKERS(executed).items()) + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + deficits.append('invalid evidence index: ' + str(exc)) + return deficits + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--verify-only', action='store_true') + parser.add_argument('--project-root', type=Path, default=ROOT) + parser.add_argument('--diagnostics', type=Path) + parser.add_argument('--offline-mirror', type=Path) + parser.add_argument('--layout-a', type=Path) + parser.add_argument('--layout-b', type=Path) + parser.add_argument('--root-composition', type=Path) + args = parser.parse_args(argv) + check_setup_budget_source((args.project_root / 'tests/fixtures/p23-03/observation.gd').read_text()) + if args.offline_mirror: + require(not any((args.verify_only, args.diagnostics, args.layout_a, args.layout_b)), + 'offline preparation must not be combined with acceptance checks') + print(json.dumps(inspect_offline_mirror(args.offline_mirror), ensure_ascii=False)) + return 2 + if args.verify_only or (os.environ.get('BUILD_OUTPUT_DIR') and not any( + (args.diagnostics, args.layout_a, args.layout_b))): + build = os.environ.get('BUILD_OUTPUT_DIR') + if not build: + print('BUILD_OUTPUT_DIR is required', file=sys.stderr) + return 2 + try: + root_roles = strict_json(args.root_composition.read_bytes()) if args.root_composition else None + deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence', root_roles) + except (OSError, ValueError) as exc: + deficits = ['invalid Root composition context: ' + str(exc)] + print(json.dumps(dict(task_id='P23-03', integrity='fail' if deficits else 'pass', + deficits=deficits, acceptance='pending'), ensure_ascii=False)) + return 1 if deficits else 0 + # Registration hashes the immutable JSON after trimming outer whitespace. + require(hashlib.sha256((ROOT / SPEC).read_text().strip().encode()).hexdigest() == SPEC_SHA, + 'immutable spec mismatch') + checked = inspect_stream(args.diagnostics.resolve()) if args.diagnostics else [] + require(bool(args.layout_a) == bool(args.layout_b), 'both layout streams required') + comparison = compare_layouts(comparison_records(args.layout_a), comparison_records(args.layout_b)) if args.layout_a else None + print(json.dumps(dict(task_id='P23-03', status='blocked', semantic_diagnostics=checked, + layout_comparison=comparison, condition_matrix=condition_matrix(), root_evidence='pending'), ensure_ascii=False)) + return 2 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_all_tests.py.source.txt b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_all_tests.py.source.txt new file mode 100644 index 0000000..502ad8c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_all_tests.py.source.txt @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +"""Exact standard suites in one private source copy, with owned native evidence. + +Completion schemas and test bodies are unchanged. Import and every script start +in a fresh private startup profile before Godot creates logs/recovery locks; +the unchanged Guard then selects a separate fresh runtime profile. +""" +import argparse, hashlib, importlib.util, json, os, re, shutil, subprocess, sys, time, uuid +from datetime import datetime, timezone +from pathlib import Path +sys.dont_write_bytecode = True +HERE = Path(__file__).resolve().parent +SUPPORT = HERE / 'regression' +MIN_SPACE = 300 * 1024 * 1024 + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + value = importlib.util.module_from_spec(spec); spec.loader.exec_module(value); return value +def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest() +def read(path): return json.loads(Path(path).read_bytes()) +def require(ok, reason): + if not ok: raise AssertionError(reason) +def write(path, value): + with Path(path).open('x', encoding='utf-8') as stream: + stream.write(json.dumps(value, ensure_ascii=False, indent=2) + '\n') +def now(): return datetime.now(timezone.utc).isoformat() +def roster(root): + return ['tests/run_tests.gd'] + sorted(str(p.relative_to(root)) for p in (root/'tests').rglob('test*.gd') + if 'export' not in p.relative_to(root).parts and str(p.relative_to(root)) != 'tests/s0/test_kernel_and_save.gd') +def inventory(base, stage=False): + base = Path(base); result = {} + if not base.exists(): return result + require(base.is_dir() and not base.is_symlink(), 'ordinary inventory directory') + for p in sorted(base.rglob('*')): + rel = p.relative_to(base) + if stage and '.godot' in rel.parts: continue + require(not p.is_symlink(), 'symlink is not an ordinary source/proof/profile') + if p.is_file(): result[rel.as_posix()] = {'bytes': p.stat().st_size, 'sha256': sha(p)} + return result +def source_refs(root): + paths = [p for folder in ('autoload','scripts','scenes','data','tests','assets','tools') + for p in (root/folder).rglob('*') if p.is_file() and p.name != '.DS_Store' and '__pycache__' not in p.parts] + paths += [root/p for p in ('project.godot','export_presets.cfg','Makefile','AGENTS.md')] + require(all(not p.is_symlink() for p in paths), 'source aliases refused') + return {p.relative_to(root).as_posix(): {'path':str(p),'bytes':p.stat().st_size,'sha256':sha(p)} for p in sorted(paths)} +def profile_path(name): + require(re.fullmatch(r'TheTree-PCK-tests-[0-9a-f]{32}-[a-z0-9-]+', name) is not None, 'owned profile nonce') + if sys.platform == 'darwin': return Path.home()/'Library/Application Support'/name + if sys.platform.startswith('linux'): return Path(os.environ.get('XDG_DATA_HOME', str(Path.home()/'.local/share')))/name + raise RuntimeError('profile platform requires a reviewed native runner; no Windows acceptance inference') +def default_player(root): + text = (root/'project.godot').read_text() + require('config/name="The Tree"' in text and 'config/use_custom_user_dir=' not in text, 'canonical default profile source') + if sys.platform == 'darwin': path = Path.home()/'Library/Application Support/Godot/app_userdata/The Tree' + elif sys.platform.startswith('linux'): path = Path(os.environ.get('XDG_DATA_HOME', str(Path.home()/'.local/share')))/'godot/app_userdata/The Tree' + else: raise RuntimeError('unreviewed default-profile platform') + return {'path':str(path),'existed':path.exists(),'files':inventory(path),'logs_and_locks_included':True} +def private_project(public_bytes, name): + text = public_bytes.decode('utf-8') + require(text.count('[application]\n\n') == 1 and 'config/use_custom_user_dir=' not in text + and 'config/custom_user_dir_name=' not in text, 'canonical project has no private execution settings') + addition = 'config/use_custom_user_dir=true\nconfig/custom_user_dir_name=' + json.dumps(name) + '\n' + altered = text.replace('[application]\n\n', '[application]\n\n' + addition, 1).encode() + require(altered.decode().replace(addition, '', 1).encode() == public_bytes, 'two private fields reverse to whole public bytes') + return altered +def fresh_environment(base): return {k:v for k,v in base.items() if not k.startswith('THE_TREE_')} +def phase_policy(test, source, policies): + value = policies.get(test, {'ordinary_cleanup_prepared':False,'ordinary_control_labels':[], + 'old_bounded_teardown':False,'new_audio_QA':False}) + if test in policies: require(value['source_sha256'] == sha(source), 'suite proof/IO policy source SHA changed') + return dict(value, path=test, source_sha256=sha(source), candidate_source_sha256=sha(source)) +def dependency_paths(test, refs): + """Reviewed literal GDScript dependencies, including relative inherited QA.""" + pending=[test];seen=set() + while pending: + name=pending.pop() + if name in seen:continue + seen.add(name) + text=Path(refs[name]['path']).read_text() + for quoted in re.findall(r'[\"\']([^\"\']+\.gd)[\"\']',text): + target=quoted[6:] if quoted.startswith('res://') else str(Path(name).parent/quoted) + if target in refs and target not in seen:pending.append(target) + return seen +def policy_coverage(paths, refs, policy): + for test in paths: + dependencies=dependency_paths(test,refs);p=phase_policy(test,Path(refs[test]['path']),policy['suite_policies']) + if 'tests/helpers/ui_audio_fixture_cleanup.gd' in dependencies: + require(p['ordinary_cleanup_prepared'] and p['ordinary_control_labels'], 'unknown ordinary helper user requires exact reviewed policy '+test) + if 'tests/helpers/ui_audio_teardown.gd' in dependencies: + require(p['ordinary_cleanup_prepared'] or p['old_bounded_teardown'] or test=='tests/ui/test_ui_audio_feedback.gd', 'unknown bounded teardown user requires reviewed proof policy '+test) +def copy_source(source,target): + if sys.platform=='darwin':subprocess.run(['/bin/cp','-c',str(source),str(target)],check=True) + else:shutil.copyfile(source,target) + require(target.stat().st_ino!=Path(source).stat().st_ino or target.stat().st_dev!=Path(source).stat().st_dev, + 'private source is a separate inode; hardlinks refused') +def profile_gate(path, report, name): + value = read(report) + require(value['verified'] is True and value['conditions'] == [True]*5 and value['conditions_checked'] == 5 + and value['fresh_before_parent_initialize'] is True and value['profile_writes_before_guard'] == 0 + and value['actual_user_data_dir'] == str(path) == value['expected_user_data_dir'] + and value['globalized_probe'] == str(path/'guard-probe') and value['custom_user_dir_name'] == name + and value['project_settings_persisted'] is False, 'all five unchanged runtime Guard conditions') + return {'report':value,'report_sha256':sha(report),'whole_files':inventory(path)} +def process_gate(row, allowed): + return (row.get('exit_code') == 0 and row.get('helper_exit_code') == 0 and row.get('timed_out') is False + and row.get('error_lines') == allowed and row.get('owned_popen_ended') is True + and row.get('old_helper_engine_ended') is True and row.get('start_receipt_written') is True + and row.get('end_receipt_written') is True and row.get('lifecycle_error') is None + and row.get('external_process_or_group_signals') == 0 + and row.get('adapter_owned_cleanup',{}).get('engine_ended') is True) +def routed_report(directory, route, test, pid, raw, stage, source_sha): + file = directory/route['filename'] + require(set(directory.iterdir()) == {file} and file.is_file() and not file.is_symlink() and file.stat().st_size>0, + 'exclusive fresh outside report file set') + value = read(file); matches = [l[len(route['stdout_prefix']):] for l in raw.splitlines() if l.startswith(route['stdout_prefix'])] + require(isinstance(value,dict) and value and len(matches)==1, 'whole report and unique actual stdout JSON') + printed = json.loads(matches[0]); require(isinstance(printed,dict) and printed, 'nonempty printed report') + require(value==printed if route['stdout_projection']=='full' else all(value.get(k)==v for k,v in printed.items()), + 'literal original report payload agreement') + owner = route.get('inherited_source',test) + require(sha(stage/test)==source_sha and sha(stage/owner)==route['report_owner_source_sha256'], 'actual report source provenance') + return {'verified':True,'path':str(file),'bytes':file.stat().st_size,'sha256':sha(file), + 'actual_engine_pid':pid,'producer_test':test,'producer_source_sha256':source_sha,'report_owner_source':owner, + 'report_owner_source_sha256':sha(stage/owner),'payload_field_deletions':0,'whole_JSON':value} +def declared_reports(stage, expected, previous, requested): + actual = inventory(stage,stage=True) + require(not set(requested)&(set(expected)|set(previous)), 'new reports cannot overwrite source/previous reports') + require(set(actual)==set(expected)|set(previous)|set(requested), 'complete source/UID/generated report set') + require({n:actual[n] for n in expected}==expected and {n:actual[n] for n in previous}==previous, 'all source and earlier report bytes exact') + added = {n:actual[n] for n in requested} + for name,pin in added.items(): + require(pin['bytes']>0, 'nonempty actual generated report') + if name.endswith('.json'): read(stage/name) + else: (stage/name).read_text() + return added +def totals(rows, paths): + names=[r['test'] for r in rows]; missing=[r['test'] for r in rows if r.get('reported_checks') is None] + remaining=[n for n in paths if n not in names]; available=sum(r.get('reported_checks') or 0 for r in rows) + return {'actual_suites':len(rows),'missing_marker_paths':missing,'remaining_unexecuted_tests':remaining, + 'available_marker_checks':available,'actual_all_roster_checks':available if not missing and names==paths else None, + 'strict_accepted_checks':sum(r['reported_checks'] for r in rows if not r['failed'] and r.get('reported_checks') is not None)} + +def execute_phase(engine, stage, out, phase, test, selected, expected, previous, policy, contracts, paths, + public_project, protected_player, root, outer, markers, diagnostics, owners, audio): + require(not outer.native.helper['live_engines'](), 'one owned native engine at a time') + require(shutil.disk_usage(out).free>=MIN_SPACE, 'fresh phase disk admission') + require(source_refs(root)==selected['root_before'] and default_player(root)==protected_player, 'entire current source/default profile unchanged before phase') + require(inventory(stage,stage=True)==dict(expected,**previous), 'whole stage source/UID/report inventory before phase') + env=fresh_environment(os.environ); startup_name='TheTree-PCK-tests-'+uuid.uuid4().hex+'-standard-startup-'+phase + startup=profile_path(startup_name); require(not startup.exists(), 'fresh startup profile before native engine') + write(out/(phase+'-startup-profile-before.json'), {'path':str(startup),'existed':False,'files':{},'before_native_spawn':True}) + private=private_project(public_project,startup_name); (stage/'project.godot').write_bytes(private) + folder=out/phase; proof=None; evidence=None; runtime=None; runtime_report=None; script=None + row={'phase':phase,'test':test,'passed':False,'failed':True,'validation_error':None,'profile_startup_name':startup_name, + 'private_project_sha256':hashlib.sha256(private).hexdigest(),'private_project_fields':2,'source_sha256':sha(stage/test) if test else None} + try: + if phase=='import': argv=[engine,'--headless','--path',str(stage),'--editor','--import','--quit','--verbose'] + else: + name='TheTree-PCK-tests-'+uuid.uuid4().hex+'-standard-runtime-'+phase; runtime=profile_path(name) + require(not runtime.exists(), 'fresh runtime profile before Guard') + runtime_report=out/(phase+'-profile.json') + env.update(THE_TREE_PACK_PROFILE_NAME=name,THE_TREE_PACK_PROFILE_EXPECTED=str(runtime), + THE_TREE_PACK_PROFILE_REPORT=str(runtime_report),THE_TREE_FULL_PARSE_PATHS=str(out/'parse-paths.json')) + if phase=='parse': script=out/'bootstrap/parse_full_dependencies.gd' + else: + script=out/'starters'/(phase+'.gd') + script.write_text('extends '+json.dumps('res://'+test)+'\nconst Guard := preload('+json.dumps(str(out/'bootstrap/profile_guard.gd'))+')\nfunc _initialize() -> void:\n\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n') + p=phase_policy(test,stage/test,policy['suite_policies']); row['policy']=p + route=policy['outside_routes'].get(test) + if route: + evidence=out/(phase+'-evidence'); evidence.mkdir(); env[route['env']]=str(evidence) + if p['ordinary_cleanup_prepared'] or p['old_bounded_teardown'] or test=='tests/ui/test_ui_audio_feedback.gd': + proof=out/(phase+'-teardown'); proof.mkdir() + if p['ordinary_cleanup_prepared']: + (proof/'owned-audio').mkdir(); env.update(THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'),THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json')) + else: env['THE_TREE_AUDIO_TEARDOWN_PROOF_DIR']=str(proof) + if test=='tests/ui/test_ui_audio_feedback.gd': + audio_dir=out/(phase+'-audio-proof'); audio_dir.mkdir() + env.update(THE_TREE_AUDIO_PROOF_REPORT=str(audio_dir/'actual-ogg-decode-player-state.json'), + THE_TREE_AUDIO_COMMAND_PROOF_REPORT=str(audio_dir/'actual-command-result-buffer-proof.json')) + argv=[engine,'--headless','--path',str(stage),'--script',str(script),'--verbose'] + logfile=folder/'engine-native.log'; require(logfile.is_absolute() and not logfile.exists(), 'fresh absolute native log') + argv+=['--log-file',str(logfile)]; row.update(command=argv,logical_script='res://'+test if test else None, + starter_sha256=sha(script) if script else None,controlled_environment={k:v for k,v in env.items() if k.startswith('THE_TREE_')}) + result,capture=outer.run_phase(argv,folder,180 if phase=='import' else selected['timeout'],env, + now_fn=now,dump_fn=write,sha_fn=sha,live_engines_fn=outer.native.helper['live_engines']) + row.update(native_process=result,outer_actual_capture=capture,actual_pid=result.get('pid') if result else None, + engine_ended=capture['all_captured_ended'],exit_code=result.get('exit_code') if result else None) + stdout=(folder/'stdout.log').read_text(); stderr=(folder/'stderr.log').read_text(); raw=stdout+stderr + raw_file=out/(test.replace('/','__')+'.log') if test else folder/'combined-raw.log'; raw_file.write_text(raw) + row.update(log=str(raw_file.relative_to(out)),log_sha256=sha(raw_file),raw_native_log={'path':str(logfile),'bytes':logfile.stat().st_size,'sha256':sha(logfile)}) + if test: + parsed=markers.parse_raw(test,raw,contracts,row['source_sha256']); row.update(completion=parsed, + reported_checks=parsed['checks'],reported_failures=parsed['failures'],completion_marker=parsed['marker'],collector_errors=parsed['errors']) + review=diagnostics.review_streams(outer.native,{'stdout':stdout,'stderr':stderr,'native_file':logfile.read_text()},test,stage/test if test else None) + row['finite_original_diagnostic_review']=review + require(result and diagnostics.diagnostic_result_gate(result,review) and process_gate(result,review['allowed_e31_error_lines']) and capture['passed'], 'strict native/ownership/logger/finite diagnostics') + if script: require(sha(script)==row['starter_sha256'], 'actual starter source remains exact') + if runtime: row['actual_private_profile']=profile_gate(runtime,runtime_report,name) + if phase=='parse': + matches=re.findall(r'(?m)^FULL_DEPENDENCY_PARSE checks=([0-9]+) failures=([0-9]+)\n',raw) + require(matches==[(str(len(paths)),'0')], 'unique actual all normal-context dependencies parsed') + row['actual_parse_checks']=int(matches[0][0]) + if test: + require(parsed['passed'] and sha(stage/test)==row['source_sha256'], 'exact original suite source/marker schema') + if evidence: row['outside_report']=routed_report(evidence,route,test,row['actual_pid'],raw,stage,row['source_sha256']) + if p['ordinary_cleanup_prepared']: row['ordinary_owner_pairs']=owners.ordinary_gate(proof,row['actual_pid'],out,p['ordinary_control_labels']) + if p['old_bounded_teardown'] or test=='tests/ui/test_ui_audio_feedback.gd': row['old_bounded_proof']=audio.teardown_gate(proof,row['actual_pid'],out) + if test=='tests/ui/test_ui_audio_feedback.gd': + row['actual_command_evidence']=audio.command_gate(audio_dir/'actual-command-result-buffer-proof.json',audio_dir,row['actual_pid']) + row['actual_ogg_evidence']=audio.ogg_gate(audio_dir/'actual-ogg-decode-player-state.json',row['actual_pid'],selected['map']) + row['passed']=True; row['failed']=False + except BaseException as caught: row['validation_error']=type(caught).__name__+': '+str(caught) + finally: + # run_phase owns cleanup before any receipts/guards. No discovered PID signals. + (stage/'project.godot').write_bytes(public_project) + row['whole_startup_profile_after']={'path':str(startup),'files':inventory(startup),'fresh_before_engine':True} + if runtime is not None: row['whole_runtime_profile_after']={'path':str(runtime),'files':inventory(runtime)} + try: + requested=policy['fixed_generated_report_routes'].get(test,[]) if test else [] + generated=declared_reports(stage,expected,previous,requested); previous.update(generated) + for name in generated: + if name.endswith('-comparison.json'): + value=read(stage/name) + require(type(value.get('checks')) is int and value['checks']==row['reported_checks'] and value.get('failures')==0, + 'actual generated JSON report/marker agreement') + row['generated_reports']={n:dict(pin,actual_engine_pid=row.get('actual_pid'),producer_test=test, + producer_source_sha256=row['source_sha256'],payload_modified=False)for n,pin in generated.items()} + row['source_sha256_end']=sha(stage/test) if test else None + row['default_player_entire_profile_unchanged']=default_player(root)==protected_player + require(row['default_player_entire_profile_unchanged'] and source_refs(root)==selected['root_before'], 'entire protected source/default player including logs/locks unchanged') + except BaseException as caught: + row['passed']=False;row['failed']=True;row['guard_error']=type(caught).__name__+': '+str(caught) + write(out/(phase+'-receipt.json'),row) + return row + +def main(argv=None): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--project-root',type=Path,default=HERE.parent); parser.add_argument('--output',type=Path) + parser.add_argument('--godot',default='godot');parser.add_argument('--timeout',type=int,default=1200) + parser.add_argument('--marker-contracts',type=Path);parser.add_argument('--source-input-refs',type=Path) + parser.add_argument('--prepare-only',action='store_true');args=parser.parse_args(argv) + root=args.project_root.resolve();out=(args.output or root/'artifacts'/('all-tests-'+uuid.uuid4().hex)).resolve();require(args.timeout>0,'positive timeout') + root_before=source_refs(root); refs=read(args.source_input_refs) if args.source_input_refs else root_before + require(set(refs)>=set(root_before), 'prospective source must retain every canonical source key') + for n,pin in refs.items(): + path=Path(pin['path']);require(not path.is_symlink() and path.is_file() and path.stat().st_size==pin['bytes'] and sha(path)==pin['sha256'], 'exact selected source input '+n) + require(refs['tools/run_all_tests.py']['sha256']==sha(Path(__file__)), 'actual public runner selected source exact') + for file in SUPPORT.iterdir(): + if file.is_file(): require(refs['tools/regression/'+file.name]['sha256']==sha(file), 'actual support selected source exact '+file.name) + paths=['tests/run_tests.gd']+sorted(n for n in refs if n.startswith('tests/') and n.endswith('.gd') and Path(n).name.startswith('test') and 'export'not in Path(n).parts and n!='tests/s0/test_kernel_and_save.gd') + markers=module('standard_exact_markers',HERE/'strict_test_markers.py');contract_path=(args.marker_contracts or root/'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json').resolve() + contracts=markers.load_contracts(contract_path);require(len(set(paths))==len(paths) and all(n in contracts for n in paths),'unique reviewed roster/schema') + policy=read(SUPPORT/'suite-policy.json') + for test in set(policy['suite_policies'])&set(paths): phase_policy(test,Path(refs[test]['path']),policy['suite_policies']) + policy_coverage(paths,refs,policy) + parse_paths=['res://'+n for n in sorted(refs) if n.endswith('.gd')] + if args.prepare_only: + print(json.dumps({'source_files':len(refs),'suites':len(paths),'parse_paths':len(parse_paths),'future_native_phases':len(paths)+2,'actual_checks':None,'engines':0,'stages':0}));return 0 + outer=module('standard_exact_owned_outer',SUPPORT/'owned_outer.py');require(not outer.native.helper['live_engines'](),'another Godot owns CPU') + engine=shutil.which(args.godot) if not Path(args.godot).is_absolute() else args.godot;require(engine,'native launcher exists') + engine=str(Path(engine).resolve());require(shutil.disk_usage(out.parent).free>=MIN_SPACE,'fresh run disk admission') + out.mkdir(parents=True,exist_ok=False);stage=out/'stage';stage.mkdir();(out/'starters').mkdir() + (out/'bootstrap').mkdir() + for name,pin in read(SUPPORT/'bootstrap-scripts.json').items(): + data=pin['text'].encode('utf-8');require(hashlib.sha256(data).hexdigest()==pin['sha256'], 'unchanged external bootstrap bytes') + (out/'bootstrap'/name).write_bytes(data) + protected=default_player(root);expected={n:{'bytes':p['bytes'],'sha256':p['sha256']}for n,p in refs.items()} + selected={'root_before':root_before,'map':{n:p['sha256']for n,p in refs.items()},'timeout':args.timeout} + for n,pin in refs.items(): + target=stage/n;target.parent.mkdir(parents=True,exist_ok=True) + copy_source(Path(pin['path']),target) + require(inventory(stage,stage=True)==expected,'one complete private CoW source copy') + public=(stage/'project.godot').read_bytes();write(out/'parse-paths.json',parse_paths) + run_id=uuid.uuid4().hex;input_receipt={'protocol':'guarded-standard-v1','run_id':run_id,'owner_runner_pid':os.getpid(), + 'project_root':str(root),'stage':str(stage),'roster':paths,'selected_source_map':selected['map'], + 'marker_contract_sha256':sha(contract_path),'runner_sha256':sha(Path(__file__)),'marker_module_sha256':sha(HERE/'strict_test_markers.py'), + 'engine':engine,'engine_sha256':sha(engine),'default_player_before':protected,'performance_is_not_maximum_scene_acceptance':True} + write(out/'runner-input.json',input_receipt);write(out/'source-start.json',root_before) + phases=[];rows=[];previous={};error=None;cleanup=[];cleanup_errors=[] + diagnostics=module('standard_finite_diagnostics',SUPPORT/'diagnostic_gate.py') + owners=module('standard_ordinary_proof_gate',SUPPORT/'ordinary_proof_gate.py');audio=module('standard_audio_proof_gate',SUPPORT/'audio_evidence_gate.py') + try: + for phase,test in [('import',None),('parse',None)]+[(str(i).zfill(3),n)for i,n in enumerate(paths)]: + row=execute_phase(engine,stage,out,phase,test,selected,expected,previous,policy,contracts,parse_paths, + public,protected,root,outer,markers,diagnostics,owners,audio);phases.append(row) + if test: + row['sequence']=len(rows);row['godot']=engine;rows.append(row) + (out/'all-tests.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2)+'\n') + print(('PASS ' if row['passed'] else 'FAIL ')+test,flush=True) + require(row['passed'],'first strict failed phase stops without retry: '+phase) + except BaseException as caught: error=type(caught).__name__+': '+str(caught) + finally: + # Sweep actual factory objects before all fragile final receipt/guard writes. + cleanup,cleanup_errors=outer.finish_captured(outer.OWNED) + context=totals(rows,paths);guard_errors=[] + try: + require(source_refs(root)==root_before,'full canonical source unchanged at end') + require(default_player(root)==protected,'full default player unchanged at end') + require(inventory(stage,stage=True)==dict(expected,**previous),'whole stage/UID/report set at end') + except BaseException as caught:guard_errors.append(type(caught).__name__+': '+str(caught)) + ended=bool(outer.OWNED) and all(r['engine_ended']is True for r in cleanup) + passed=error is None and not cleanup_errors and not guard_errors and ended and len(phases)==len(paths)+2 and all(r['passed']for r in phases) + write(out/'runner-ended.json',dict(context,protocol='guarded-standard-v1',run_id=run_id,owner_runner_pid=os.getpid(), + passed=passed,engine_ended=ended,failed_suites=sum(r['failed']for r in rows),actual_captured_PIDs=[p.pid for p in outer.OWNED], + final_owned_cleanup=cleanup,cleanup_errors=cleanup_errors,guard_errors=guard_errors,error=error, + all_tests_sha256=sha(out/'all-tests.json') if (out/'all-tests.json').exists() else None,phases=phases, + default_player_after=default_player(root),formal_acceptance=False,native_Windows='pending')) + print(json.dumps(dict(context,passed=passed,error=error,cleanup_errors=cleanup_errors,guard_errors=guard_errors)),flush=True) + return 0 if passed else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_p23_life_observation.py.source.txt b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_p23_life_observation.py.source.txt new file mode 100644 index 0000000..0a25514 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_p23_life_observation.py.source.txt @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +"""Root-only native collection. Does not run regression or claim acceptance.""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import time +import uuid +import re + +sys.dont_write_bytecode = True +HERE = Path(__file__).resolve().parent +CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' +CONDITIONS = ('room_occupancy_and_operation', 'queues_and_overflow', + 'elevator_state_and_passengers', 'staff_state_and_jobs', + 'supplies_and_carried_goods', 'same_demand_route_improvement', + 'normal_os_and_distinct_pid_resume') +OS_CHILD_TIMEOUT_SECONDS = 3600 +RECORDER = 'tests/fixtures/p23-03/passive_os_recorder.gd' + +def collect_screenshots(out, pair, role): + """Bind every external CUA screenshot to its request and decoded PNG. + + The external receipt supplies screenshots[{path,raw_sha256,tool_call_id}]. + Paths are relative to the evidence root; raw receipt bytes remain unchanged. + """ + checker = module('p23_screenshot_binding', HERE/'check_p23_life_observation_evidence.py') + bindings = [] + requests = json.loads((pair/(role+'-requests.json')).read_text())['requests'] + for ack_name in requests: + ack_path = out/ack_name + checker.require(ack_path.resolve().is_relative_to(out.resolve()), 'ACK path escape') + ack = json.loads(ack_path.read_text()) + request_path = ack_path.with_name('request.json') + request = json.loads(request_path.read_text()) + receipt_path = out/ack['input_receipt'] + checker.require(receipt_path.resolve().is_relative_to(out.resolve()), 'CUA receipt path escape') + cua = json.loads(receipt_path.read_text()) + checker.require(ack['request_sha256'] == checker.sha(request_path), 'CUA request hash') + checker.require(cua['screenshots'], 'CUA screenshots missing') + for number, shot in enumerate(cua['screenshots']): + raw = out/shot['path'] + checker.require(raw.resolve().is_relative_to(out.resolve()) and not raw.is_symlink(), 'screenshot path escape') + checker.require(checker.sha(raw) == shot['raw_sha256'] and shot['tool_call_id'], 'raw screenshot CUA binding') + kind, _, _ = checker.image_kind(raw.read_bytes()) + conversion = None + if kind == 'jpeg': + png = ack_path.with_name('decoded-%02d.png' % number) + receipt = ack_path.with_name('conversion-%02d.json' % number) + convert_screenshot(raw.resolve(), png.resolve(), shot, receipt.resolve()) + conversion = str(receipt.relative_to(out)) + else: + png = raw + width, height, _ = checker.png_pixels(png.read_bytes()) + bindings.append(dict(request=str(request_path.relative_to(out)), request_sha256=checker.sha(request_path), + sequence=request['sequence'], target_pid=request['target_pid'], + input_receipt=ack['input_receipt'], input_receipt_sha256=checker.sha(receipt_path), + screenshot_index=number, tool_call_id=shot['tool_call_id'], + raw_path=shot['path'], raw_sha256=shot['raw_sha256'], conversion=conversion, + png_path=str(png.relative_to(out)), png_sha256=checker.sha(png), width=width, height=height)) + checker.require(bindings, 'empty screenshot collection') + destination = pair/(role+'-screenshots.json') + with destination.open('x') as output: + json.dump(bindings, output, indent=2) + output.write('\n') + return str(destination.relative_to(out)) + +def convert_screenshot(raw_path, png_path, cua_receipt, receipt_path): + """Root-only conversion; raw CUA bytes are never renamed or rewritten.""" + checker = module('p23_conversion_checker', HERE/'check_p23_life_observation_evidence.py') + raw, png, receipt = map(Path, (raw_path, png_path, receipt_path)) + checker.require(raw.is_absolute() and png.is_absolute() and receipt.is_absolute(), + 'absolute conversion paths required') + checker.require(raw.is_file() and not raw.is_symlink() and not png.exists() + and not receipt.exists() and raw.resolve() != png.resolve(), 'fresh separate conversion output') + raw_hash = checker.sha(raw) + checker.require(raw_hash == cua_receipt['raw_sha256'] and cua_receipt['tool_call_id'], + 'CUA conversion input hash/call mismatch') + kind, width, height = checker.image_kind(raw.read_bytes()) + checker.require(kind == 'jpeg', 'conversion requires JPEG input') + converter = '/usr/bin/sips' + converter_hash = checker.sha(converter) + argv = [converter, '-s', 'format', 'png', str(raw), '--out', str(png)] + process = subprocess.run(argv, capture_output=True, timeout=900, check=False) + rec = dict(raw_path=str(raw), raw_sha256=raw_hash, raw_kind=kind, + tool_call_id=cua_receipt['tool_call_id'], converter=converter, + converter_sha256=converter_hash, argv=argv, exit_code=process.returncode, + stdout_sha256=hashlib.sha256(process.stdout).hexdigest(), + stderr_sha256=hashlib.sha256(process.stderr).hexdigest(), + png_path=str(png), png_sha256=checker.sha(png) if png.is_file() else None, + width=width, height=height) + # Preserve failure receipts too. These logs allow independent hash checking. + with receipt.open('x') as output: + json.dump(rec, output, indent=2) + output.write('\n') + with Path(str(receipt) + '.stdout').open('xb') as output: + output.write(process.stdout) + with Path(str(receipt) + '.stderr').open('xb') as output: + output.write(process.stderr) + checker.require(checker.sha(raw) == raw_hash, 'raw CUA bytes changed during conversion') + checker.require(checker.sha(converter) == converter_hash, 'converter changed during conversion') + checker.inspect_conversion(rec, cua_receipt, converter_hash) + return rec + +def collect_os(r, root, out, stage, engine, commit, refs, protected, outer): + """Adapted P08-03 collect_os protocol, retaining owned-process guards. + + Root supplies CUA ack/receipt files in the requested directories. Neither + controller nor passive endpoints generate input or change permissions. + """ + r.require(not outer.native.helper['live_engines'](), 'external engine present') + checker = module('p23_os_pins', HERE/'check_p23_life_observation_evidence.py') + before = r.inventory(stage, stage=True) + public = (stage/'project.godot').read_bytes() + (out/'os-pair').mkdir() + phases = [] + for label in ('A-writer', 'A-reader', 'B-writer', 'B-reader'): + layout, role = label.split('-') + pair = out/'os-pair'/layout + pair.mkdir(exist_ok=role == 'reader') + r.require(not outer.native.helper['live_engines'](), 'external engine present') + r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'OS before guards') + r.require(r.inventory(stage, stage=True) == before, 'OS stage before guard') + r.require(shutil.disk_usage(out).free >= 500*1024*1024, 'OS disk admission') + folder = out/'os-runs'/label + folder.mkdir(parents=True) + name = 'TheTree-PCK-tests-'+uuid.uuid4().hex+'-p23-observation-'+label.lower() + profile = r.profile_path(name) + r.require(not profile.exists(), 'fresh OS private profile') + formal_source = pair/'formal' if role == 'reader' else None + formal_before = r.inventory(formal_source) if formal_source else {} + if role == 'reader': + r.require(set(formal_before) == {'world.bin','manifest.json','thumbnail.png','journal.log'}, 'all formal inputs') + formal = profile/'saves/manual_ui' + formal.mkdir(parents=True) + for member, pin in formal_before.items(): + r.copy_source(formal_source/member, formal/member) + r.require(r.sha(formal/member) == pin['sha256'], 'formal fixture copy SHA') + recorder = folder/'passive_os.gd' + r.copy_source(root/RECORDER, recorder) + proof = folder/'teardown' + (proof/'owned-audio').mkdir(parents=True) + env = r.fresh_environment(dict(os.environ)) + env.update(P23_OBSERVATION_ROOT=str(pair), P23_OBSERVATION_ROLE=role, P23_LAYOUT=layout, + THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'), + THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json')) + command = [engine, '--path', str(stage), '--script', str(recorder)] + base = {'protocol':'p23-observation-native-os-v1', 'role':role, 'layout':layout, 'commit':commit, + 'source_map':{n:v['sha256'] for n,v in refs.items()}, 'stage':str(stage), + 'stage_before':before, 'profile':str(profile), 'profile_name':name, + 'profile_before':{'existed':profile.exists(), 'files':r.inventory(profile) if profile.exists() else {}}, 'formal_input':formal_before, + 'formal_source':str(formal_source) if formal_source else None, 'owner_pid':os.getpid(), + 'default_before':protected, 'command':command, 'engine_sha256':r.sha(engine), + 'controller_sha256':r.sha(root/'tools/run_p23_life_observation.py'), + 'recorder_sha256':r.sha(recorder), 'formal_acceptance':False, + 'timeout_seconds':OS_CHILD_TIMEOUT_SECONDS, + 'inputs':checker.input_map(root), 'spec_sha256':checker.SPEC_SHA, + 'settings_sha256':hashlib.sha256(public).hexdigest(), + 'environment':{k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))}, + 'environment_sha256':hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest()} + r.write(folder/'started.json', base) + error = None; children = []; guards = []; exit_code = None; owner_proof = None + try: + (stage/'project.godot').write_bytes(r.private_project(public, name)) + with (folder/'engine-native.log').open('x') as log: + process = subprocess.Popen(command, cwd=stage, env=env, stdout=log, stderr=subprocess.STDOUT) + children.append(process) + r.write(folder/'process-started.json', dict(base, actual_pid=process.pid, + monotonic_ns=time.monotonic_ns(), state='waiting_for_root_cua_requests')) + print(json.dumps({'actual_pid':process.pid, 'request_directory':str(pair), + 'state':'waiting_for_root_cua', 'role':role}), flush=True) + exit_code = process.wait(timeout=base['timeout_seconds']) + r.require(exit_code == 0, 'ordinary native OS exit0') + raw = (folder/'engine-native.log').read_text() + r.require('ERROR:' not in raw and 'SCRIPT ERROR:' not in raw, 'OS native diagnostics') + matches = re.findall(r'(?m)^P23 passive OS observation: ([0-9]+) checks, 0 failures$', raw) + r.require(len(matches) == 1 and int(matches[0]) > 0, 'passive observer completion') + observed = r.read(pair/(role+'-profile.json')) + r.require(observed['pid'] == process.pid and Path(observed['user_data_dir']).resolve() == profile.resolve() + and observed['display'] != 'headless', 'actual OS private profile/display') + collect_screenshots(out, pair, role) + owners = r.module('p23_os_owner_gate', r.SUPPORT/'ordinary_proof_gate.py') + owner_proof = owners.ordinary_gate(proof, process.pid, folder, ['single']) + except BaseException as caught: + error = type(caught).__name__+': '+str(caught) + finally: + try: + cleanup, cleanup_errors = outer.finish_captured(children) + finally: + (stage/'project.godot').write_bytes(public) + try: + r.require(r.source_refs(root) == refs, 'OS source unchanged') + r.require(r.inventory(stage, stage=True) == before, 'OS stage unchanged') + r.require(r.default_player(root) == protected, 'OS default player unchanged') + if formal_source: + r.require(r.inventory(formal_source) == formal_before, 'formal input unchanged') + r.require(r.sha(recorder) == base['recorder_sha256'], 'recorder unchanged') + except BaseException as caught: + guards.append(type(caught).__name__+': '+str(caught)) + passed = not (error or guards or cleanup_errors) and len(children) == 1 and exit_code == 0 + r.write(folder/'ended.json', dict(base, actual_pid=children[0].pid if children else None, + exit_code=exit_code, engine_ended=bool(cleanup) and all(v['engine_ended'] for v in cleanup), + error=error, guard_errors=guards, cleanup_errors=cleanup_errors, owned_cleanup=cleanup, + ordinary_owner_proof=owner_proof, stage_after=r.inventory(stage,stage=True), + engine_sha256_after=r.sha(engine), inputs_after=checker.input_map(root), + settings_sha256_after=hashlib.sha256((stage/'project.godot').read_bytes()).hexdigest(), + environment_after={k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))}, + environment_sha256_after=hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest(), + default_after=r.default_player(root), source_after=r.source_refs(root), + profile_after=r.inventory(profile), passed=passed)) + phases.append('os-runs/'+label+'/ended.json') + r.require(passed, 'native OS collection failed: '+str(error or guards or cleanup_errors)) + return phases + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + obj = importlib.util.module_from_spec(spec) + spec.loader.exec_module(obj) + return obj + +def absolute(value): + path = Path(value) + if not path.is_absolute(): + raise argparse.ArgumentTypeError('absolute path required') + return path.resolve() + +def collect(args): + root, out = args.project_root, args.output + if out.is_relative_to(root): + raise ValueError('evidence output must be outside the frozen source checkout') + r = module('p23_standard_runner', root/'tools/run_all_tests.py') + commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() + r.require(commit == args.commit, 'actual tested commit Q required') + r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required') + r.require(not out.exists(), 'fresh output; never overwrite earlier proof') + refs = r.source_refs(root) + checker = module('p23_initial_inputs', root/'tools/check_p23_life_observation_evidence.py') + inputs_before = checker.input_map(root) + root_roles = checker.strict_json(args.root_composition.read_bytes()) + composition = checker.verify_composition(root, root_roles, + {name: pin['sha256'] for name, pin in refs.items()}, inputs_before) + outer = r.module('p23_owned_outer', r.SUPPORT/'owned_outer.py') + r.require(not outer.native.helper['live_engines'](), 'one native engine') + engine = shutil.which(args.godot) + r.require(engine is not None, 'native engine exists') + engine = str(Path(engine).resolve()) + out.mkdir(parents=True) + stage = out/'stage'; stage.mkdir() + (out/'starters').mkdir(); (out/'bootstrap').mkdir() + for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items(): + data = pin['text'].encode() + r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin') + (out/'bootstrap'/name).write_bytes(data) + for name, pin in refs.items(): + target = stage/name; target.parent.mkdir(parents=True, exist_ok=True) + r.copy_source(Path(pin['path']), target) + r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name) + expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()} + selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180} + r.require(r.inventory(stage, stage=True) == expected, 'complete private source') + protected = r.default_player(root); public = (stage/'project.godot').read_bytes() + paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')] + r.write(out/'parse-paths.json', paths) + markers = r.module('p23_strict_markers', root/'tools/strict_test_markers.py') + contracts = markers.load_contracts(root/CONTRACT) + policy = r.read(r.SUPPORT/'suite-policy.json') + diagnostics = r.module('p23_diagnostics', r.SUPPORT/'diagnostic_gate.py') + owners = r.module('p23_owners', r.SUPPORT/'ordinary_proof_gate.py') + audio = r.module('p23_audio', r.SUPPORT/'audio_evidence_gate.py') + r.write(out/'runner-input.json', {'protocol':'p23-normal-observation-v1', 'commit':commit, + 'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine), + 'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':806042, + 'inputs':inputs_before, 'provenance':checker.input_provenance(root), + 'pixel_decoder':{'interpreter':sys.executable, + 'PIL_available':importlib.util.find_spec('PIL') is not None, + 'raw_screenshots':'preserve bytes; Root lossless conversion required for JPEG without PIL'}}) + phases = []; previous = {}; cases = []; error = None; os_proof = None + deadline = time.monotonic()+900 + saved_env = {k:os.environ.get(k) for k in ('P23_LAYOUT','P23_OUTPUT')} + def phase(label, test): + remaining = int(deadline-time.monotonic()) + r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline') + selected['timeout'] = min(180, remaining) + phase_out = out/'runs'/label + phase_out.mkdir(parents=True) + (phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir() + for original in (out/'bootstrap').iterdir(): + r.copy_source(original,phase_out/'bootstrap'/original.name) + r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy') + r.write(phase_out/'parse-paths.json',paths) + row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous, + policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio) + # Supplement the existing runner receipt; preserve it verbatim. + r.write(phase_out/'p23-inputs.json', dict(source_map=selected['map'], source_after=r.source_refs(root), + engine_sha256=r.sha(engine), inputs=inputs_before, inputs_after=checker.input_map(root), + spec_sha256=checker.SPEC_SHA, settings_sha256=hashlib.sha256(public).hexdigest(), + stage_before=expected, stage_after=r.inventory(stage,stage=True), + default_before=protected, default_after=r.default_player(root), + receipt_sha256=r.sha(phase_out/(label+'-receipt.json')))) + phases.append('runs/'+label+'/'+label+'-receipt.json') + r.require(not row['failed'], 'strict phase failed: '+label) + try: + phase('import', None); phase('parse', None) + for layout in ('A', 'B'): + directory = out/('layout-'+layout) + os.environ.update(P23_LAYOUT=layout, P23_OUTPUT=str(directory)) + phase('layout-'+layout.lower(), 'tests/ui/test_p23_life_observation.gd') + cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)}) + for layout in ('A', 'B'): + checker.inspect_stream(out/('layout-'+layout), root) + if args.input_mode == 'native_os': + os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer) + checker = module('p23_os_input_map', root/'tools/check_p23_life_observation_evidence.py') + os_proof = 'os-proof.json' + r.write(out/os_proof, {'source_map':selected['map'], 'inputs':checker.input_map(root), + 'input_method':'native_os', 'observer_mutations':[], 'phases':os_phases, 'directory':'os-pair'}) + except BaseException as caught: + error = type(caught).__name__+': '+str(caught) + finally: + cleanup, cleanup_errors = outer.finish_captured(outer.OWNED) + for key,value in saved_env.items(): + if value is None: os.environ.pop(key, None) + else: os.environ[key] = value + if r.source_refs(root) != refs or r.default_player(root) != protected: + error = (error+'; ' if error else '')+'protected root/profile changed' + if checker.input_map(root) != inputs_before: + error = (error+'; ' if error else '')+'spec/fixture/controller inputs changed' + r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors, + 'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'}) + checker = module('p23_observation_checker', root/'tools/check_p23_life_observation_evidence.py') + r.write(out/'index.json', {'schema':'p23-life-observation-v2', 'commit':commit, + 'composition':composition, + 'source_map':selected['map'], 'input_map':inputs_before, + 'provenance':checker.input_provenance(root), + 'marker_contract_sha256':r.sha(root/CONTRACT), + 'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P23-03.json').read_text().strip().encode()).hexdigest(), + 'cases':cases, 'phases':phases, 'conditions':list(CONDITIONS), + 'artifacts':{str(p.relative_to(out)):r.sha(p) + for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts}, + 'os_proof':os_proof, 'regression':None, + 'acceptance':'pending', 'collection_error':error}) + return 1 if error or cleanup_errors else 0 + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--project-root', type=absolute, required=True) + parser.add_argument('--output', type=absolute, required=True) + parser.add_argument('--commit', required=True) + parser.add_argument('--root-composition', type=absolute, required=True, + help='Root external S/M/T/Q and complete tested-tree blob bindings') + parser.add_argument('--godot', default='godot') + parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler') + parser.add_argument('--collect', action='store_true', required=True) + return collect(parser.parse_args(argv)) + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/LOCAL_COMMAND_RESULTS.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/LOCAL_COMMAND_RESULTS.json new file mode 100644 index 0000000..8a5e255 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/LOCAL_COMMAND_RESULTS.json @@ -0,0 +1,92 @@ +{ + "scope": "SOURCE-ONLY; stdlib/Git, TEST-ONLY synthetic fixtures; no acceptance", + "commands": [ + { + "name": "py_compile", + "argv": [ + "/Applications/Xcode.app/Contents/Developer/usr/bin/python3", + "-m", + "py_compile", + "tools/check_p24_operations_evidence.py", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/correspondence.py", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py" + ], + "exit_code": 0, + "expected_exit_code": 0, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/py_compile.log", + "sha256": "169f24774e13a79b6341d6c41c58f159e180701e87e9028aa70afdf8712b6fbb" + }, + { + "name": "import", + "argv": [ + "/Applications/Xcode.app/Contents/Developer/usr/bin/python3", + "-c", + "import importlib.util; s=importlib.util.spec_from_file_location('p24','tools/check_p24_operations_evidence.py'); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); print('stdlib checker import OK; ROOT='+str(m.ROOT))" + ], + "exit_code": 0, + "expected_exit_code": 0, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/import.log", + "sha256": "4a2fe8c27abe4d9c077ca3701b07ee860d4673f93267e4ae4697638a1f2254c9" + }, + { + "name": "repair5_tests", + "argv": [ + "/Applications/Xcode.app/Contents/Developer/usr/bin/python3", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py" + ], + "exit_code": 0, + "expected_exit_code": 0, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair5_tests.log", + "sha256": "48a4c44bd1f5f94faa0f78f14afea371f04d4e9652c89fc8dcad91a24e7cf836" + }, + { + "name": "repair4_tests", + "argv": [ + "/Applications/Xcode.app/Contents/Developer/usr/bin/python3", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py" + ], + "exit_code": 0, + "expected_exit_code": 0, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair4_tests.log", + "sha256": "d3852846638752b6f8421776da825a037228ead8c67333c6b14e93a2fffb8f7a" + }, + { + "name": "preservation", + "argv": [ + "/Applications/Xcode.app/Contents/Developer/usr/bin/python3", + "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement/docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py" + ], + "exit_code": 0, + "expected_exit_code": 0, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/preservation.log", + "sha256": "7e6691b7ae2ae94f956758a8907d3d67db29b9642ed0aecbfbc94421dcbdbace" + }, + { + "name": "missing_real_evidence", + "argv": [ + "/Applications/Xcode.app/Contents/Developer/usr/bin/python3", + "tools/check_p24_operations_evidence.py", + "--verify-only" + ], + "exit_code": 1, + "expected_exit_code": 1, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/missing_real_evidence.log", + "sha256": "e1f4250f0078f0a88e8fd4ec485386ab67e835d0e966e609def115de00f4c48c" + }, + { + "name": "diff_check", + "argv": [ + "git", + "diff", + "--check" + ], + "exit_code": 0, + "expected_exit_code": 0, + "raw_log": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/diff_check.log", + "sha256": "169f24774e13a79b6341d6c41c58f159e180701e87e9028aa70afdf8712b6fbb" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/PROTOCOL.md b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/PROTOCOL.md new file mode 100644 index 0000000..9cf90f3 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/PROTOCOL.md @@ -0,0 +1,31 @@ +# Repair5 source/local handoff (not acceptance) + +The existing collector_protocol.json and operations_os_observer.gd remain authoritative about scope: the observer's local_unit_fixture_only_not_native_os_input_evidence summaries are never OS acceptance. No actual index is included. No engine executable path has been invented. TEST-ONLY fixtures exercise validators, not the game. + +## Composition and paths + +Run the checker from ROOT or a directory within the same checkout. ROOT is obtained with scrubbed Git from the checker location. Relative sources, suite roster and artifacts never use cwd. The temporary tested view is materialized from verified T blobs within the existing task TMPDIR. The source map is exactly run_all_tests.source_refs' filter on T, including fixed settings and every source/asset/test/tool. The marker JSON (outside that filter) is mandatory in inputs. The checker helpers themselves must also be in inputs, as must the frozen spec, project.godot and suite-policy.json. Root can add other actual Git-file inputs; blob_bindings must equal the union of source_hashes and inputs, without omissions or extras. Bindings are SHA1 blob IDs, and source_hashes/inputs are SHA256 of the bytes. + +The regular, non-symlink evidence-dir/root-composition.json is SHA256-bound by index.root_composition_sha256. Its S, M, Q and T are actual Git object IDs; mode is normal_validation or integration_final. Q parents must be exactly [M,S]. Normal validation runs at HEAD=S; integration_final additionally binds C=HEAD, its exact [M,S] parents, a clean checkout and C tree=T. CANDIDATE_COMMIT/TARGET_HEAD only detect mismatches. + +Composition runs merge-tree M S in a private object directory and index, with repository objects as alternates. It accepts exactly the four content conflicts, replaces precisely those entries with S modes/blobs and compares write-tree with Q/T. Every M-only and nonconflicting merged entry survives. Private objects/index are deleted even on failure. The actual Main54 four input blobs are pinned in composition.py; changing any requires new reconciliation and a reviewed source-pin update. A docs-only M requires fresh Q parents and the actual new deterministic tree, never relabelling an old Q. + +The generic functions re-express P23 reference blob 8a62d1e1070688ced445e48d49b831fc7e9cfea3. P23 identities and runtime receipts are not reused as P24 evidence. No P23 module is imported. + +## External engine and record correspondence + +index.engine is {path, sha256, version}, separate from Git bindings. The actual regular external file is hashed without executing it. The OS receipt must carry the identical engine object, engine_started=true, engine_ended=true and strictly increasing started_monotonic_ns/ended_monotonic_ns. Root supplies the genuine path/version and runner receipt. All artifact/record provenance has root, S, T, source_map_sha256, pid, session_id and engine. source_map_sha256 hashes canonical UTF-8 JSON with sorted keys, compact separators and ensure_ascii=false. The OS receipt and os_environment share pid/session. Cross-process records additionally contain actual producer/consumer runtime receipts with different PIDs and identical manifest/world/source/engine bindings. + +Each condition retains its prior passed/details/artifact_ids and content guards, and adds coverage. The exact meaning vocabulary is MEANINGS in correspondence.py, derived clause by clause from the frozen eight groups. Each coverage value is {artifact_id, pointer}, where pointer is a canonical /events/N JSON pointer into the existing OS format {action, monotonic_ns, ...observations}. The action names identify the individual meaning, never PASS. Observed fields listed in MEANINGS are supplementary collector data; they are not emitted by the old local observer and must be genuinely collected by Root. A flag cannot replace them. Multiple conditions may reference distinct records in one genuine OS trace. All listed artifacts must participate, except the required runtime receipt which is intrinsically used. Same bytes under alias IDs, reused records, relabelled actions, missing meanings, mismatched provenance, out-of-runtime events and fixture/template payloads fail. + +Bubble records carry the actual observer shape {actor_id, kind, source_state, recorded_at}, plus OS render observations bound to the same actor/kind. Cutin records carry {kind, heading, message, recorded_at}, plus a production trigger and domain-event trace. These extra OS observations do not turn local record_bubble/record_cutin calls into production evidence. Final full-roster records retain the exact suite-set/source/results checks and require the same started/ended engine identity on all rows. Existing Japanese/IME, owned save path, pause/restart and Windows-not-accepted guards remain in place. + +The unit-level test_only adapter is inaccessible from the CLI. It requires explicit TEST-ONLY fixture_scope and is used solely for synthetic validator controls. Actual verification rejects those controls. Static validity of the correspondence structure is not authentication of a human review or proof of gameplay: Root's independent review must inspect the actual raw receipts and observations. + +## UI and remaining scope + +DefenseManager now connects visibility_changed before add_child, matching the existing dialogs. The added ordinary-cleanup test opens _show_defense_manager, attempts behind-dialog build/drop input on the real model, compares complete snapshots (including economy ledger) and selections, then closes and checks unblocking. It has not run in Godot. + +The direct assign_staff_by_click test is not OS map-click evidence. Later OS collection must exercise the staff panel facility_option/apply_button route. No new map interaction or signal wiring was added. Bubble render/state transitions remain native/collector work. First-relationship order, the inherited heart rule and the O(N²) relationship scan are pending whole-eight review candidates, not new gameplay values or mandated optimizations. + +The existing repair4 preservation helper now permits exactly the current P24 test SHA rebind. All other marker/policy values/order are preserved. It writes its historical result only with --write-evidence. Its historical set-based body comparison remains limited; optional full sequence hardening was not undertaken. Repair5 tests separately compare the entire main.gd delta to exactly the single callback line, world_preview byte-for-byte, the entire test outside the enumerated inserted block, and both JSON structures after undoing only the P24 pin. This does not re-prove P23. diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/REFERENCE_INTEGRITY.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/REFERENCE_INTEGRITY.json new file mode 100644 index 0000000..c109a1a --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/REFERENCE_INTEGRITY.json @@ -0,0 +1,30 @@ +{ + "reference_only_not_runtime_proof": true, + "files": [ + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/check_p23_life_observation_evidence.py.source.txt", + "sha256": "e41479ae51bccdfab3f31bd348794b441c2f341179bbe33222e2aacdf88f987f", + "matches_manifest": true + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_p23_life_observation.py.source.txt", + "sha256": "8dbd90e22f47912b3381b514398fe1fb2c403c97d8e3d0dd675e9c8db5b5d0d1", + "matches_manifest": true + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/tools/run_all_tests.py.source.txt", + "sha256": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4", + "matches_manifest": true + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/actual-limited-review/result.json", + "sha256": "ef17de03039bcd2b3d7a2851d26c0abdc163b32fef40048d933a693f1b49114f", + "matches_manifest": true + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/root-repair5-context-20261017/main-reference/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair4-20261017/CURRENT_ACCEPTANCE_AND_NEXT_BATCH.json", + "sha256": "9c7892ce6ccd26d997e7a609be29ef8a12acb2fb7df75cf4def6f11a8a9157dc", + "matches_manifest": true + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/SOURCE_MANIFEST.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/SOURCE_MANIFEST.json new file mode 100644 index 0000000..e9887d8 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/SOURCE_MANIFEST.json @@ -0,0 +1,134 @@ +{ + "task_id": "P24-02", + "existing_HEAD": "bd3a13405115fc9bf6c5acec55bcfef7dd15266a", + "status": "SOURCE-READY/NOTRUN", + "note": "Uncommitted source/local phase only. This manifest excludes itself. Supplied Root references are unchanged and excluded from authored files.", + "files": [ + { + "path": "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json", + "bytes": 189076, + "sha256": "d3599139e2358c1ac9edb5059a823880f72efe5ff457da2887114c243139f743", + "git_blob_sha1": "932e8c4e8e6113aac8b629197d757a73a6f1260e" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py", + "bytes": 22108, + "sha256": "e7dc30bcd56dcdf3f057131bf5dc9a3a672e357bc9b8e5bf2b818d3302d4c83a", + "git_blob_sha1": "a2259ee3ec1d0b981504e446c3fc6e213fde8f9f" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py", + "bytes": 5689, + "sha256": "5c10efe7a9c6b3d290020f8dfbb2431956b3dafff7abdd4859b8e6d83e1d9365", + "git_blob_sha1": "f1a07ecf07b94f1508efa98a8606eeeaa61a7a46" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/LOCAL_COMMAND_RESULTS.json", + "bytes": 4909, + "sha256": "aacde68f9d6909c1c347197b257fa31a66d3e7519c01e16c79fd45aba6e8d82f", + "git_blob_sha1": "8a5e25505d56f5f41e40ef426a69b54900689f92" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/PROTOCOL.md", + "bytes": 6620, + "sha256": "c07618fd692f911120e70ac13e3de4addf379aa8d29e535867ff93d9bd32e8f6", + "git_blob_sha1": "9cf90f38f067c67f09b7540ae475f341f97fa7a9" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/REFERENCE_INTEGRITY.json", + "bytes": 1695, + "sha256": "be8549f3f6462757c7fb1bc664ccd6c5165f09e2eee4468aec75a8d84e4addf9", + "git_blob_sha1": "c109a1ab6c7a954faff4897ca5bb37f3b2fd0fe8" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/STATUS.json", + "bytes": 3620, + "sha256": "b27c9bf3d75e59544528605413d4253b0c88e91298f6fb6c561a12b3aa0b26bd", + "git_blob_sha1": "2fbc3d66cf7de3061da20bf67adc4ba84c1cb5ed" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py", + "bytes": 13130, + "sha256": "b2fc058b1589310499917c8c5fad056fd8861ccf6bb32dd3ce205d367f493864", + "git_blob_sha1": "85c7157858eae2f0019ca578aaad12af3c09eecd" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/correspondence.py", + "bytes": 12934, + "sha256": "7a0eda4900bc405569b9a797b1f80446e6210a77a81c3c7967f639364c493821", + "git_blob_sha1": "f3bce62d865da58e0bbfd3a1101b20909579d3f3" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/diff_check.log", + "bytes": 16, + "sha256": "169f24774e13a79b6341d6c41c58f159e180701e87e9028aa70afdf8712b6fbb", + "git_blob_sha1": "fa146f5e868243f2f30da886ba8857913f7ba42a" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/import.log", + "bytes": 137, + "sha256": "4a2fe8c27abe4d9c077ca3701b07ee860d4673f93267e4ae4697638a1f2254c9", + "git_blob_sha1": "733ed0946b27fc409db84e600a3ec15a8dc25945" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/missing_real_evidence.log", + "bytes": 134, + "sha256": "e1f4250f0078f0a88e8fd4ec485386ab67e835d0e966e609def115de00f4c48c", + "git_blob_sha1": "c40aa5563c4816cfb29faf41da7f53ba1131b07b" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/preservation.log", + "bytes": 927, + "sha256": "7e6691b7ae2ae94f956758a8907d3d67db29b9642ed0aecbfbc94421dcbdbace", + "git_blob_sha1": "ec333d287202ab01d6a9a3df8faf9cc9906ad7a1" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/py_compile.log", + "bytes": 16, + "sha256": "169f24774e13a79b6341d6c41c58f159e180701e87e9028aa70afdf8712b6fbb", + "git_blob_sha1": "fa146f5e868243f2f30da886ba8857913f7ba42a" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair4_tests.log", + "bytes": 634, + "sha256": "d3852846638752b6f8421776da825a037228ead8c67333c6b14e93a2fffb8f7a", + "git_blob_sha1": "1731dbb82f16a817c95f69f34ceb2d4da7fad12c" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair5_tests.log", + "bytes": 1656, + "sha256": "48a4c44bd1f5f94faa0f78f14afea371f04d4e9652c89fc8dcad91a24e7cf836", + "git_blob_sha1": "a03710c0d634ad1da8e7ada00f91296c1808cdae" + }, + { + "path": "docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py", + "bytes": 21118, + "sha256": "f6e00851b444b018a176588f3077d98ab146c64fc0c1ac1dba98bf4dadc7542d", + "git_blob_sha1": "77064fcbac9866b00f73aa062cda26a8d9aa4102" + }, + { + "path": "scenes/main/main.gd", + "bytes": 146784, + "sha256": "35c3cec2d1d13f56c1635010a5811c853d7fb42e13b8d2af6cfe8261832be6c6", + "git_blob_sha1": "5d136195c7e28a060cde7473ab76baf46b7949e8" + }, + { + "path": "tests/ui/test_operations_notification.gd", + "bytes": 18698, + "sha256": "50172ab6daa031d221f6a78cac5cdbcb8884f295f8062b54656562e2412faff2", + "git_blob_sha1": "15dc9e63b0167a0809a949087c23d79a9189623c" + }, + { + "path": "tools/check_p24_operations_evidence.py", + "bytes": 38965, + "sha256": "0c4699fa032e9961e8e9965000382301cfd55898131ddad463ec8a38024d7818", + "git_blob_sha1": "57a1b34e94295a687a05cb0f1af60f62545fee45" + }, + { + "path": "tools/regression/suite-policy.json", + "bytes": 17581, + "sha256": "93178d9f68b15307158f1050993bdc75d8790d65ebe75400a3572978e37ec26a", + "git_blob_sha1": "d0bddce72981ae73d11a56e07b0e326d56fe9a46" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/STATUS.json b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/STATUS.json new file mode 100644 index 0000000..2fbc3d6 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/STATUS.json @@ -0,0 +1,80 @@ +{ + "task_id": "P24-02", + "source": "bd3a13405115fc9bf6c5acec55bcfef7dd15266a", + "phase": "non-UI source/local repair5", + "acceptance_ids": [], + "native_runs": 0, + "full_roster_runs": 0, + "game_completion_credit": 0, + "conditions": { + "placement_and_invalid_drop": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Native placement/rejection/ledger/selection and actual staff-panel OS click coverage NOTRUN." + }, + "modal_and_pause_resume": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Defense modal test source added; native execution, pending-command/save identity, ordinary pause/resume and actual separate-process save restart NOTRUN." + }, + "unit_bubbles": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Bubble native render/model/state switching and removal NOTRUN. First-relationship order, heart semantics and O(N²) scan remain whole-eight review candidates." + }, + "event_log": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Current exact-source production bounded log/Japanese sequence/amount/time/reason/pause/load evidence NOTRUN; historical evidence is not current acceptance." + }, + "important_warning_cutin": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Natural NTR/duel/defense production triggers require whole P18-01/02, P19, P21 inputs. P18 backend 265ce30 is a completed ancestor and is not restarted. Unknown production initial values remain unresolved; no new values added." + }, + "click_alternatives": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Actual staff-panel facility_option/apply_button OS click and navigation coverage NOTRUN. Direct handler calls are not map-click evidence. P23 held gestures remain deferred." + }, + "japanese_input_and_scale": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "OS IME composition/confirm/cancel, persisted save display, scale/focus/clipping and old-save protection NOTRUN. Same-Windows-release device evidence unavailable and still required." + }, + "final_validation_review": { + "source_status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "accepted": false, + "gap": "Root must independently review this source batch, rebuild fresh Q/T on actual current M, run import/impact, normal OS collection, one exact final full roster, whole-eight review and normal integration/final. No actual engine path/receipt/index supplied or fabricated." + } + }, + "denominator": { + "packages": 30, + "units": 88, + "completed": 14, + "partial_credit": false + }, + "optional4": "Only --write-evidence flag behavior clarified; historical sequence comparison hardening remains deferred.", + "root_next_gate": "Independent Claude limited source review; actual new Q/T composition and all actual runtime/evidence gates remain Root-owned.", + "local_checks": { + "py_compile_exit": 0, + "python_import_exit": 0, + "repair5_stdlib_git_tests": 21, + "repair5_exit": 0, + "preserved_roster_head_tests": 9, + "repair4_exit": 0, + "preservation_exit": 0, + "diff_check_exit": 0, + "missing_real_evidence_exit": 1, + "raw_results": "LOCAL_COMMAND_RESULTS.json", + "source_hashes": "SOURCE_MANIFEST.json" + } +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py new file mode 100644 index 0000000..85c7157 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py @@ -0,0 +1,262 @@ +"""Stdlib/Git composition primitives; no runtime or acceptance execution. + +Re-expressed from P23 checker blob 8a62d1e1070688ced445e48d49b831fc7e9cfea3, +SHA256 e41479ae51bccdfab3f31bd348794b441c2f341179bbe33222e2aacdf88f987f. +Source filter matches run_all_tests.source_refs (reference blob +502ad8c1f6ba371446ecfd8f37f0f9194cdb10ea); no imports from Main-only modules. +""" +import contextlib +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile + +CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' +SPEC = 'docs/spec/acceptance/P24-02-code-r01.json' +RECONCILED_MAIN = '54cade4181f6c3c4f5701ba54ac5fcbeca695945' +MAIN_INPUTS = { + 'scenes/main/main.gd': 'fff37f082d750bb91c71f2aa54d168c9e2cb8fe7', + 'scenes/main/world_preview.gd': 'fa32fe0344d20565b6d542da1256a94e8dee1845', + CONTRACT: '2eecb803f5eb6774ff38cd353de1f02847aeb0f9', + 'tools/regression/suite-policy.json': 'ad194ae425fb8123fa191ed3635f404c098f692d', +} +KNOWN_CONFLICTS = set(MAIN_INPUTS) +OVERRIDES = {'GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_COMMON_DIR'} + + +def require(ok, message): + if not ok: + raise ValueError(message) + + +def clean_env(): + # Config injection can also redirect Git. Do not inherit any Git overrides. + return {k: v for k, v in os.environ.items() if not k.startswith('GIT_')} + + +def git_process(root, *args, private=None, input=None): + env = clean_env() + if private: + env.update(private) + return subprocess.run(['git', '-C', str(root), *args], input=input, + capture_output=True, env=env, timeout=120) + + +def git_bytes(root, *args, private=None): + p = git_process(root, *args, private=private) + require(p.returncode == 0, 'Git failed: ' + ' '.join(args) + ': ' + p.stderr.decode(errors='replace')) + return p.stdout + + +def root_identity(script_root, cwd): + root = Path(git_bytes(script_root, 'rev-parse', '--show-toplevel').decode().strip()).resolve() + require(Path(git_bytes(cwd, 'rev-parse', '--show-toplevel').decode().strip()).resolve() == root, + 'cwd differs from ROOT') + head = git_bytes(root, 'rev-parse', 'HEAD').strip() + if any(k in OVERRIDES or k.startswith('GIT_CEILING') for k in os.environ): + inherited = subprocess.run(['git', '-C', str(root), 'rev-parse', 'HEAD'], + capture_output=True, timeout=10) + require(inherited.returncode == 0 and inherited.stdout.strip() == head, + 'inherited Git override changes HEAD') + for name in ('CANDIDATE_COMMIT', 'TARGET_HEAD'): + require(name not in os.environ or os.environ[name].strip().encode() == head, + name + ' mismatches HEAD') + return root + + +def strict_json(raw): + def pairs(rows): + d = {} + for k, v in rows: + require(k not in d, 'duplicate JSON key: ' + k) + d[k] = v + return d + return json.loads(raw, object_pairs_hook=pairs) + + +def safe_path(root, name): + p = Path(name) + require(not p.is_absolute() and '..' not in p.parts and p.as_posix() == name, 'unsafe relative path') + target = root / p + require(target.resolve().is_relative_to(root.resolve()), 'path escapes root') + require(not any((root / Path(*p.parts[:i])).is_symlink() for i in range(1, len(p.parts)+1)), + 'source/artifact symlink') + return target + + +def root_context(directory, index): + path = safe_path(directory, 'root-composition.json') + require(path.is_file(), 'missing regular root-composition.json') + raw = path.read_bytes() + require(hashlib.sha256(raw).hexdigest() == index.get('root_composition_sha256'), 'composition SHA mismatch') + return strict_json(raw) + + +def blob_oid(raw): + return hashlib.sha1(b'blob %d\0' % len(raw) + raw).hexdigest() + + +def git_blob_stream(root, oids): + process = subprocess.Popen(['git', '-C', str(root), 'cat-file', '--batch'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=clean_env()) + try: + for oid in oids: + require(re.fullmatch('[0-9a-f]{40}', oid), 'invalid blob OID') + process.stdin.write((oid + '\n').encode()); process.stdin.flush() + match = re.fullmatch(rb'([0-9a-f]{40}) blob (0|[1-9][0-9]*)\n', process.stdout.readline(128)) + require(match is not None and match[1].decode() == oid, 'invalid blob header') + raw = process.stdout.read(int(match[2])) + require(len(raw) == int(match[2]) and process.stdout.read(1) == b'\n', 'truncated blob') + require(blob_oid(raw) == oid, 'blob SHA1 mismatch') + yield oid, raw + process.stdin.close() + require(process.stdout.read(1) == b'' and process.wait() == 0, 'extra blob output/Git failure') + finally: + if process.poll() is None: + process.kill() + process.wait() + process.stdin.close(); process.stdout.close() + + +def tree(root, revision): + result = {} + for row in git_bytes(root, 'ls-tree', '-r', '-z', revision).split(b'\0'): + if not row: + continue + meta, path = row.split(b'\t', 1) + path = path.decode() + mode, kind, oid = meta.decode().split() + require(path not in result, 'duplicate tree path') + require(not Path(path).is_absolute() and '..' not in Path(path).parts, 'unsafe tree path') + result[path] = (mode, kind, oid) + return result + + +def source_paths(manifest): + folders = {'autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools'} + fixed = {'project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md'} + require(fixed <= set(manifest), 'missing fixed source settings') + return {p for p in manifest if (Path(p).parts[0] in folders and Path(p).name != '.DS_Store' + and '__pycache__' not in Path(p).parts)} | fixed + + +@contextlib.contextmanager +def tested_checkout(root, t): + # Only a private writable allocation; never fall back to global temp. + temporary = os.environ.get('TMPDIR') + require(temporary and Path(temporary).is_dir(), 'existing task TMPDIR required') + with tempfile.TemporaryDirectory(prefix='p24-view-', dir=temporary) as name: + view = Path(name) + manifest = tree(root, t) + paths = sorted(manifest) + for path, (_, raw) in zip(paths, git_blob_stream(root, [manifest[p][2] for p in paths])): + require(manifest[path][0] in ('100644', '100755') and manifest[path][1] == 'blob', + 'non-regular tested source') + target = safe_path(view, path) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(raw) + yield view + + +def deterministic_tree(root, m, s, main_inputs=MAIN_INPUTS): + main, submitted = tree(root, m), tree(root, s) + require(set(main_inputs) == KNOWN_CONFLICTS, 'reconciliation must bind all four inputs') + for p, oid in main_inputs.items(): + require(main.get(p, (None, None, None))[2] == oid, + 'Main conflict input changed; new reconciliation required: ' + p) + temporary = os.environ.get('TMPDIR') + require(temporary and Path(temporary).is_dir(), 'existing task TMPDIR required') + objects = Path(git_bytes(root, 'rev-parse', '--git-path', 'objects').decode().strip()) + if not objects.is_absolute(): + objects = root / objects + with tempfile.TemporaryDirectory(prefix='p24-compose-', dir=temporary) as name: + private = Path(name) + (private / 'objects').mkdir() + env = {'GIT_OBJECT_DIRECTORY': str(private / 'objects'), + 'GIT_ALTERNATE_OBJECT_DIRECTORIES': str(objects.resolve()), + 'GIT_INDEX_FILE': str(private / 'index')} + result = git_process(root, 'merge-tree', '--write-tree', '-z', '--name-only', m, s, private=env) + require(result.returncode == 1, 'expected exactly four content conflicts') + fields = result.stdout.split(b'\0') + merged = fields.pop(0).decode() + require(re.fullmatch('[0-9a-f]{40}', merged), 'missing merge tree') + conflict_paths = [] + while fields and fields[0]: + conflict_paths.append(fields.pop(0).decode()) + require(fields and fields.pop(0) == b'', 'missing conflict separator') + require(len(conflict_paths) == 4 and set(conflict_paths) == KNOWN_CONFLICTS, 'unexpected conflict set') + # Structured merge-tree messages: path count, paths, type, message. + seen = set() + while fields and fields[0]: + count = int(fields.pop(0)) + paths = [fields.pop(0).decode() for _ in range(count)] + kind = fields.pop(0).decode() + message = fields.pop(0).decode() + if kind == 'Auto-merging': + continue + require(kind == 'CONFLICT (contents)' and count == 1 and paths[0] in KNOWN_CONFLICTS + and 'binary' not in message.lower(), 'unsupported conflict type: ' + kind) + seen.update(paths) + require(seen == KNOWN_CONFLICTS, 'missing content conflict messages') + for p in KNOWN_CONFLICTS: + require(main[p][0] == submitted[p][0] == '100644' + and main[p][1] == submitted[p][1] == 'blob', 'mode/type conflict') + git_bytes(root, 'read-tree', merged, private=env) + for p in sorted(KNOWN_CONFLICTS): + mode, _, oid = submitted[p] + git_bytes(root, 'update-index', '--add', '--cacheinfo', mode, oid, p, private=env) + return git_bytes(root, 'write-tree', private=env).decode().strip() + + +def verify_composition(root, roles, source_map, inputs, base, allowed_paths, main_inputs=MAIN_INPUTS): + require(isinstance(roles, dict), 'missing composition roles') + for k in ('S', 'M', 'Q', 'T'): + require(isinstance(roles.get(k), str) and re.fullmatch('[0-9a-f]{40}', roles[k]), 'invalid role ' + k) + s, m, q, t = (roles[k] for k in ('S', 'M', 'Q', 'T')) + for c in (base, s, m, q): + require(git_bytes(root, 'cat-file', '-t', c).strip() == b'commit', 'role is not commit') + require(git_bytes(root, 'rev-list', '--parents', '-n', '1', q).decode().split() == [q, m, s], + 'Q parents must be exactly current [M,S]') + require(git_bytes(root, 'rev-parse', q+'^{tree}').decode().strip() == t, 'T differs from Q tree') + head = git_bytes(root, 'rev-parse', 'HEAD').decode().strip() + if roles.get('mode') == 'normal_validation': + require(head == s, 'normal validation HEAD must be S') + elif roles.get('mode') == 'integration_final': + require(roles.get('C') == head, 'final HEAD must be C') + require(git_bytes(root, 'rev-list', '--parents', '-n', '1', head).decode().split() == [head, m, s], + 'C parents must be exactly [M,S]') + require(git_bytes(root, 'rev-parse', head+'^{tree}').decode().strip() == t, 'C tree differs') + require(not git_bytes(root, 'status', '--porcelain'), 'final checkout dirty') + else: + raise ValueError('invalid composition mode') + require(deterministic_tree(root, m, s, main_inputs) == t, 'Q/T differs from deterministic composition') + b, st, mt, tt = (tree(root, rev) for rev in (base, s, m, t)) + changed_s = {p for p in b.keys() | st.keys() if b.get(p) != st.get(p)} + changed_m = {p for p in b.keys() | mt.keys() if b.get(p) != mt.get(p)} + def allowed(p): + return any(p.startswith(a[:-2]) if a.endswith('/**') else p == a for a in allowed_paths) + require(all(allowed(p) for p in changed_s), 'S changed outside allowed_paths') + for paths, authority in ((changed_s-changed_m, st), (changed_m-changed_s, mt), (KNOWN_CONFLICTS, st)): + require(all(tt.get(p) == authority.get(p) for p in paths), 'S/M-only or conflict provenance mismatch') + require(isinstance(source_map, dict) and set(source_map) == source_paths(tt), 'incomplete source_refs map') + require(isinstance(inputs, dict) and {SPEC, CONTRACT, 'project.godot', 'tools/regression/suite-policy.json'} <= set(inputs), + 'missing settings/spec/marker/policy input pins') + required = set(source_map) | set(inputs) + bindings = roles.get('blob_bindings') + require(isinstance(bindings, dict) and set(bindings) == required, 'missing/extra blob_bindings') + paths = sorted(required) + for p in paths: + require(p in tt and tt[p][0] in ('100644', '100755') and tt[p][1] == 'blob', 'not regular bound blob') + require(bindings[p] == tt[p][2], 'changed blob binding: ' + p) + for p, (oid, raw) in zip(paths, git_blob_stream(root, [tt[p][2] for p in paths])): + digest = hashlib.sha256(raw).hexdigest() + require(blob_oid(raw) == bindings[p] == oid, 'SHA1 binding mismatch') + for mapping in (source_map, inputs): + require(p not in mapping or mapping[p] == digest, 'SHA256 binding mismatch: ' + p) + if head != s or p in changed_s-changed_m or p in KNOWN_CONFLICTS: + require(safe_path(root, p).read_bytes() == raw, 'checkout bytes mismatch: ' + p) + return roles diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/correspondence.py b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/correspondence.py new file mode 100644 index 0000000..f3bce62 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/correspondence.py @@ -0,0 +1,198 @@ +"""Record correspondence, using existing OS events {action, monotonic_ns} and +observer bubble_history/modal_records/cutin_records/save_records payloads. +No PASS record is evidence. Coverage is an index into raw records, not a result. +""" +import hashlib +import json +from pathlib import Path + +# Each frozen clause has its own required meaning. The values are the substantive +# observed fields required alongside the existing OS action/timestamp envelope. +MEANINGS = { + 'placement_and_invalid_drop': { + 'grid_snap': ('pointer_position', 'cell', 'grid_geometry'), + 'connection_warning': ('connection', 'warning_text'), + 'valid_commit_once': ('before', 'after', 'command_ids'), + 'invalid_rejection_with_reason_and_model_ledger_selection_unchanged': ('reason', 'before', 'after'), + 'click_equivalent': ('control_path', 'before', 'after')}, + 'modal_and_pause_resume': { + 'modal_open_cancel_confirm_focus': ('dialog_name', 'transitions', 'focus'), + 'pending_commands_save_identity': ('before', 'after'), + 'behind_dialog_build_assignment_unchanged': ('before', 'after', 'inputs'), + 'pause_freezes': ('before', 'after', 'elapsed'), + 'resume_once': ('before', 'after', 'elapsed'), + 'cross_process_restart': ('producer', 'consumer', 'manifest_sha256', 'world_sha256')}, + 'unit_bubbles': { + 'zzz_state_model_render': ('actor_id', 'source_state', 'render'), + 'anger_state_model_render': ('actor_id', 'source_state', 'render'), + 'heart_state_model_render': ('actor_id', 'relationship', 'render'), + 'switching_removal': ('actor_id', 'before', 'after', 'render')}, + 'event_log': { + 'production_sequence_bounded256': ('domain_events', 'rows', 'recent_limit'), + 'japanese_amount_time_reason': ('rows', 'render'), + 'pause_load_no_duplicates': ('before', 'after', 'event_ids')}, + 'important_warning_cutin': { + 'natural_invasion_start': ('production_trigger', 'domain_event', 'heading', 'message'), + 'natural_duel_start': ('production_trigger', 'domain_event', 'heading', 'message'), + 'natural_defense_start': ('production_trigger', 'domain_event', 'heading', 'message'), + 'dismiss_stop_resume': ('transitions', 'before', 'after')}, + 'click_alternatives': { + 'navigation_zoom_pan_reset_grid_layer': ('controls', 'before', 'after'), + 'staff_panel_assignment': ('facility_option', 'apply_button', 'before', 'after'), + 'presentation_model_unchanged': ('before', 'after'), + 'held_gestures_deferred': ('remaining_scope', 'source_reference')}, + 'japanese_input_and_scale': { + 'ime_composition': ('text', 'composition_string', 'cursor_position'), + 'ime_confirmation': ('text', 'confirmed_text'), + 'ime_cancellation': ('before', 'after'), + 'persisted_japanese_display': ('display_name', 'path', 'loaded_display_name'), + 'invalid_input_feedback': ('input_text', 'reason'), + 'default_owned_path_old_save_cancel': ('before', 'after', 'path'), + 'scale_clipping_focus_modal': ('window_size', 'display_scale', 'render', 'focus', 'dialog_name'), + 'macos_windows_classification': ('platform', 'windows_status')}, + 'final_validation_review': { + 'static_import_local_affected': ('commands', 'results'), + 'exact_final_full_roster': ('results', 'source_hashes', 'inputs'), + 'independent_claude_runtime_review': ('reviewer', 'review', 'runtime_artifact_sha256'), + 'current_main_integration_final': ('S', 'M', 'Q', 'T', 'mode')}, +} + + +def require(ok, reason): + if not ok: + raise ValueError(reason) + + +def canonical_sha(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()).hexdigest() + + +def pointer(value, path): + require(isinstance(path, str) and path.startswith('/') and '~' not in path, 'noncanonical record pointer') + for part in path[1:].split('/'): + if isinstance(value, list): + require(part.isdigit() and str(int(part)) == part, 'noncanonical array pointer') + value = value[int(part)] + else: + value = value[part] + require(isinstance(value, dict), 'pointer is not a record') + return value + + +def substantive(value): + if isinstance(value, bool) or value is None: + return False + if isinstance(value, dict): + return bool(value) and any(substantive(v) for v in value.values()) + if isinstance(value, list): + return bool(value) and any(substantive(v) for v in value) + return isinstance(value, (str, int, float)) and value != '' + + +def verify_engine(engine, receipt): + require(isinstance(engine, dict) and set(engine) == {'path', 'sha256', 'version'}, 'external engine fingerprint') + path = Path(engine['path']) + require(path.is_absolute() and path.is_file() and not path.is_symlink(), 'actual regular engine path required') + require(hashlib.sha256(path.read_bytes()).hexdigest() == engine['sha256'], 'engine bytes mismatch') + require(isinstance(engine['version'], str) and bool(engine['version'].strip()), 'engine version required') + require(receipt.get('engine') == engine, 'receipt engine mismatch') + require(receipt.get('engine_started') is True and receipt.get('engine_ended') is True, + 'actual started+ended engine receipt required') + require(type(receipt.get('started_monotonic_ns')) is int + and type(receipt.get('ended_monotonic_ns')) is int + and 0 < receipt['started_monotonic_ns'] < receipt['ended_monotonic_ns'], 'runtime interval required') + + +def verify(data, artifacts, roles, root, *, test_only=False): + if test_only: + require(data.get("fixture_scope") == "TEST-ONLY", "test adapter requires labelled data") + require(set(data['conditions']) == set(MEANINGS), 'whole-eight required') + os_env = data['os_environment'] + receipt_id = os_env['receipt_artifact_id'] + docs = {aid: json.loads(row['file'].read_bytes()) for aid, row in artifacts.items()} + receipt = docs[receipt_id] + verify_engine(data.get('engine'), receipt) + require(receipt.get('session_id') == os_env.get('session_id') and isinstance(receipt.get('session_id'), str) + and receipt['session_id'], 'OS session binding missing') + expected = {'root': str(root.resolve()), 'S': roles['S'], 'T': roles['T'], + 'source_map_sha256': canonical_sha(data['source_hashes']), + 'pid': os_env['pid'], 'session_id': os_env['session_id'], 'engine': data['engine']} + hashes = [hashlib.sha256(row['file'].read_bytes()).hexdigest() for row in artifacts.values()] + require(len(set(hashes)) == len(hashes), 'artifact alias/identical bytes') + for aid, doc in docs.items(): + require(isinstance(doc, dict) and doc.get('provenance') == expected, 'artifact root/source/session/engine mismatch: '+aid) + raw = json.dumps(doc).lower() + require((test_only and doc.get('fixture_scope') == 'TEST-ONLY') or not any(word in raw for word in ('test-only', 'local_unit_fixture_only', 'scaffold', 'pending-template')), + 'fixture/template is not actual evidence') + used = {receipt_id} + seen_records = set() + for condition, meanings in MEANINGS.items(): + coverage = data['conditions'][condition].get('coverage') + require(isinstance(coverage, dict) and set(coverage) == set(meanings), 'missing/extra meaning: '+condition) + for meaning, fields in meanings.items(): + link = coverage[meaning] + require(isinstance(link, dict) and set(link) == {'artifact_id', 'pointer'}, 'coverage must point to raw record') + aid, ptr = link['artifact_id'], link['pointer'] + require(aid in docs, 'unknown coverage artifact') + record = pointer(docs[aid], ptr) + require(aid in data['conditions'][condition]['artifact_ids'], 'coverage artifact absent from condition') + require((aid, ptr) not in seen_records, 'record reused for different meanings') + seen_records.add((aid, ptr)); used.add(aid) + # Existing OS trace event format is action + monotonic_ns. Native + # observer records alone have no OS action and cannot prove these clauses. + require(ptr.startswith('/events/') and ptr.count('/') == 2, 'coverage must point to an OS event') + require(record.get('action') == meaning, 'OS action does not cover meaning') + require(type(record.get('monotonic_ns')) is int + and receipt['started_monotonic_ns'] <= record['monotonic_ns'] <= receipt['ended_monotonic_ns'], + 'record outside receipt runtime') + require(record.get('provenance') == expected, 'record provenance mismatch') + require(all(k in record and substantive(record[k]) for k in fields), 'opaque/boolean-only evidence: '+meaning) + if meaning in ('invalid_rejection_with_reason_and_model_ledger_selection_unchanged', + 'behind_dialog_build_assignment_unchanged', 'pending_commands_save_identity', + 'presentation_model_unchanged'): + keys = ('model', 'ledger', 'selection') if 'unchanged' in meaning else ('pending_commands', 'save_identity') + require(all(k in record['before'] and record['before'][k] == record['after'].get(k) for k in keys), + 'state preservation mismatch') + if condition == 'unit_bubbles': + observed = record.get('observer_record') + require(isinstance(observed, dict) and set(observed) == {'actor_id', 'kind', 'source_state', 'recorded_at'}, + 'actual observer bubble payload required') + require(observed['actor_id'] == record['actor_id'] and type(observed['source_state']) is int + and type(observed['recorded_at']) is int, 'bubble observer identity/state/timestamp') + kinds = {'zzz_state_model_render': 'zzz', 'anger_state_model_render': 'anger', + 'heart_state_model_render': 'heart', 'switching_removal': ''} + require(observed['kind'] == kinds[meaning], 'bubble kind does not cover meaning') + require(isinstance(record['render'], dict) and record['render'].get('actor_id') == observed['actor_id'] + and record['render'].get('kind') == observed['kind'], 'bubble render/model binding') + if meaning.startswith('natural_'): + observed = record.get('observer_record') + kind = meaning.removeprefix('natural_').removesuffix('_start') + require(isinstance(observed, dict) and set(observed) == {'kind', 'heading', 'message', 'recorded_at'} + and observed['kind'] == kind and observed['heading'] == record['heading'] + and observed['message'] == record['message'] and type(observed['recorded_at']) is int, + 'actual cutin observer payload required') + require(isinstance(record['production_trigger'], dict) and isinstance(record['domain_event'], dict) + and record['domain_event'].get('event_id') and record['domain_event'].get('event_type'), + 'production trigger/event trace required; injected signals insufficient') + if meaning == 'cross_process_restart': + producer, consumer = record['producer'], record['consumer'] + require(isinstance(producer, dict) and isinstance(consumer, dict), 'actual restart receipts required') + for child in (producer, consumer): + verify_engine(data['engine'], child) + require(type(child.get('pid')) is int and child['pid'] > 1, 'restart PID') + require(child.get('S') == roles['S'] and child.get('T') == roles['T'] + and child.get('source_map_sha256') == expected['source_map_sha256'], 'restart source binding') + require(all(child.get(k) == record[k] for k in ('manifest_sha256', 'world_sha256')), 'restart save mismatch') + require(producer['pid'] != consumer['pid'], 'restart must be a separate process') + if meaning == 'exact_final_full_roster': + require(record['source_hashes'] == data['source_hashes'] and record['inputs'] == data['inputs'], 'roster input identity') + require(isinstance(record['results'], list) and record['results'], 'actual roster rows') + for row in record['results']: + verify_engine(data['engine'], row) + if meaning == 'valid_commit_once': + require(isinstance(record['command_ids'], list) and len(record['command_ids']) == 1, + 'placement must commit once') + if meaning == 'macos_windows_classification': + require(record['windows_status'] == 'NOTRUN' and record['platform'] == os_env['platform'], + 'Windows must remain separate, not accepted') + require(used == set(artifacts), 'unreferenced filler artifacts') diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/diff_check.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/diff_check.log new file mode 100644 index 0000000..fa146f5 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/diff_check.log @@ -0,0 +1,2 @@ + +--- STDERR --- diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/import.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/import.log new file mode 100644 index 0000000..733ed09 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/import.log @@ -0,0 +1,3 @@ +stdlib checker import OK; ROOT=/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement + +--- STDERR --- diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/missing_real_evidence.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/missing_real_evidence.log new file mode 100644 index 0000000..c40aa55 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/missing_real_evidence.log @@ -0,0 +1,4 @@ + +--- STDERR --- +ERROR: Could not locate p24-operations-evidence index.json. +Ensure BUILD_OUTPUT_DIR is set or provide --index . diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/preservation.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/preservation.log new file mode 100644 index 0000000..ec333d2 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/preservation.log @@ -0,0 +1,37 @@ +{ + "status": "SOURCE-READY", + "runtime_status": "NOTRUN", + "files": { + "scenes/main/main.gd": { + "required_count": 279, + "reconciled_symbol_count": 279, + "missing": 0, + "duplicates": 0, + "unexpected": 0, + "body_issues": 0 + }, + "scenes/main/world_preview.gd": { + "required_count": 179, + "reconciled_symbol_count": 180, + "missing": 0, + "duplicates": 0, + "unexpected": 0, + "body_issues": 0 + }, + "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json": { + "missing": 0, + "duplicates": 0, + "equals_main_plus_p24_only_additions_in_order": true + }, + "tools/regression/suite-policy.json": { + "missing": 0, + "duplicates": 0, + "equals_main_plus_p24_only_additions_in_order": true + } + }, + "schema_checks_ok": true, + "root_composition_dependencies": 4, + "failures": [] +} + +--- STDERR --- diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/py_compile.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/py_compile.log new file mode 100644 index 0000000..fa146f5 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/py_compile.log @@ -0,0 +1,2 @@ + +--- STDERR --- diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair4_tests.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair4_tests.log new file mode 100644 index 0000000..1731dbb --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair4_tests.log @@ -0,0 +1,16 @@ + +--- STDERR --- +test_1_invented_replacement (__main__.CheckerTests) ... ok +test_2_duplicate_padding (__main__.CheckerTests) ... ok +test_3_unknown_extra (__main__.CheckerTests) ... ok +test_4_required_results (__main__.CheckerTests) ... ok +test_5_source_hash (__main__.CheckerTests) ... ok +test_6_missing_or_malformed_rows (__main__.CheckerTests) ... ok +test_7_env_mismatch_and_positive_head (__main__.CheckerTests) ... ok +test_8_git_unavailable_env_only (__main__.CheckerTests) ... ok +test_positive_roster (__main__.CheckerTests) ... ok + +---------------------------------------------------------------------- +Ran 9 tests in 0.473s + +OK diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair5_tests.log b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair5_tests.log new file mode 100644 index 0000000..a03710c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/repair5_tests.log @@ -0,0 +1,28 @@ + +--- STDERR --- +test_altered_conflict_nonconflict_and_main_only (__main__.CompositionTests) ... ok +test_bindings_and_source_map (__main__.CompositionTests) ... ok +test_docs_only_main_fresh_q (__main__.CompositionTests) ... ok +test_fifth_conflict (__main__.CompositionTests) ... ok +test_final_mode_clean_exact_parents (__main__.CompositionTests) ... ok +test_git_environment_and_crosschecks (__main__.CompositionTests) ... ok +test_new_main_old_q_and_stale_source_new_q (__main__.CompositionTests) ... ok +test_outside_scope (__main__.CompositionTests) ... ok +test_parent_order_extra_and_tree_mismatch (__main__.CompositionTests) ... ok +test_positive_normal_and_private_objects (__main__.CompositionTests) ... ok +test_root_context_sha_symlink_and_cwd (__main__.CompositionTests) ... ok +test_engine_requires_actual_receipt_and_bytes (__main__.CorrespondenceTests) ... ok +test_observer_payloads_and_pointer (__main__.CorrespondenceTests) ... ok +test_alias_identical_bytes (__main__.CoverageAttackTests) ... ok +test_all_true_opaque_and_scaffold (__main__.CoverageAttackTests) ... ok +test_cross_process_and_whole_roster_engine (__main__.CoverageAttackTests) ... ok +test_missing_meaning_reused_and_relabelled_record (__main__.CoverageAttackTests) ... ok +test_old_catchall_plus_seven_fillers (__main__.CoverageAttackTests) ... ok +test_one_shared_trace_distinct_records_positive (__main__.CoverageAttackTests) ... ok +test_wrong_root_source_and_engine (__main__.CoverageAttackTests) ... ok +test_exact_nonp24_and_schema_preservation (__main__.PreservationTests) ... ok + +---------------------------------------------------------------------- +Ran 21 tests in 10.582s + +OK diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py new file mode 100644 index 0000000..77064fc --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/test_repair5.py @@ -0,0 +1,328 @@ +"""TEST-ONLY synthetic stdlib/Git controls. Never acceptance/native evidence.""" +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[5] + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + result = importlib.util.module_from_spec(spec); spec.loader.exec_module(result) + return result + +c = module('composition', HERE/'composition.py') +e = module('correspondence', HERE/'correspondence.py') +checker = module('checker', ROOT/'tools/check_p24_operations_evidence.py') + +class CompositionTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(dir=os.environ['TMPDIR'], prefix='TEST-ONLY-p24-') + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + self.git('init', '-q') + self.git('config', 'user.name', 'TEST-ONLY') + self.git('config', 'user.email', 'test-only@example.invalid') + for p in c.KNOWN_CONFLICTS | {c.SPEC, 'project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md', 'scripts/both.txt'}: + self.write(p, 'TEST-ONLY base\n') + self.base = self.commit('base') + for p in c.KNOWN_CONFLICTS: + self.write(p, 'TEST-ONLY Main\n') + self.write('docs/main-only.txt', 'TEST-ONLY Main-only\n') + self.m = self.commit('Main') + self.pins = {p: c.tree(self.root, self.m)[p][2] for p in c.KNOWN_CONFLICTS} + self.git('checkout', '-q', self.base) + for p in c.KNOWN_CONFLICTS: + self.write(p, 'TEST-ONLY Source\n') + self.write('scripts/source-only.txt', 'TEST-ONLY Source-only\n') + self.s = self.commit('Source') + # Build Q independently with Git merge-tree/private index, then commit-tree + # only in this throwaway TEST-ONLY repository. + merge = c.git_process(self.root, 'merge-tree', '--write-tree', '-z', '--name-only', self.m, self.s) + self.assertEqual(merge.returncode, 1) + self.git('read-tree', merge.stdout.split(b'\0')[0].decode()) + for p in c.KNOWN_CONFLICTS: + entry = c.tree(self.root, self.s)[p] + self.git('update-index', '--cacheinfo', entry[0], entry[2], p) + self.t = self.git('write-tree') + self.q = self.git('commit-tree', self.t, '-p', self.m, '-p', self.s, '-m', 'TEST-ONLY Q') + self.git('read-tree', self.s) + self.roles = dict(S=self.s, M=self.m, Q=self.q, T=self.t, mode='normal_validation') + tt = c.tree(self.root, self.t) + self.sources = {p: hashlib.sha256(c.git_bytes(self.root, 'show', self.t+':'+p)).hexdigest() for p in c.source_paths(tt)} + self.inputs = {p: hashlib.sha256(c.git_bytes(self.root, 'show', self.t+':'+p)).hexdigest() + for p in (c.SPEC, c.CONTRACT, 'project.godot', 'tools/regression/suite-policy.json')} + self.roles['blob_bindings'] = {p: tt[p][2] for p in self.sources.keys() | self.inputs.keys()} + self.allowed = list(c.KNOWN_CONFLICTS) + ['scripts/source-only.txt'] + + def git(self, *args): + return c.git_bytes(self.root, *args).decode().strip() + + def write(self, p, text): + target = self.root/p; target.parent.mkdir(parents=True, exist_ok=True); target.write_text(text) + + def commit(self, message): + self.git('add', '.'); self.git('commit', '-qm', 'TEST-ONLY '+message) + return self.git('rev-parse', 'HEAD') + + def verify(self, roles=None, pins=None): + return c.verify_composition(self.root, roles or self.roles, self.sources, self.inputs, + self.base, self.allowed, pins or self.pins) + + def altered_q(self, path, text): + self.git('read-tree', self.t) + blob = c.git_process(self.root, 'hash-object', '-w', '--stdin', input=text.encode()).stdout.decode().strip() + self.git('update-index', '--cacheinfo', '100644', blob, path) + t = self.git('write-tree'); self.git('read-tree', self.s) + q = self.git('commit-tree', t, '-p', self.m, '-p', self.s, '-m', 'TEST-ONLY malicious Q') + return dict(self.roles, T=t, Q=q) + + def test_positive_normal_and_private_objects(self): + before = sorted(p.relative_to(self.root/'.git/objects').as_posix() for p in (self.root/'.git/objects').rglob('*')) + self.verify() + self.assertEqual(before, sorted(p.relative_to(self.root/'.git/objects').as_posix() for p in (self.root/'.git/objects').rglob('*'))) + with c.tested_checkout(self.root, self.t) as view: + self.assertEqual((view/'docs/main-only.txt').read_text(), 'TEST-ONLY Main-only\n') + + def test_altered_conflict_nonconflict_and_main_only(self): + for p in ('scenes/main/main.gd', 'scripts/both.txt', 'docs/main-only.txt'): + with self.subTest(path=p), self.assertRaisesRegex(ValueError, 'deterministic'): + self.verify(self.altered_q(p, 'TEST-ONLY attacker\n')) + self.git('read-tree', self.t) + self.git('update-index', '--force-remove', 'docs/main-only.txt') + t = self.git('write-tree'); self.git('read-tree', self.s) + q = self.git('commit-tree', t, '-p', self.m, '-p', self.s, '-m', 'TEST-ONLY dropped') + with self.assertRaises(ValueError): self.verify(dict(self.roles, T=t, Q=q)) + + def test_parent_order_extra_and_tree_mismatch(self): + for parents in ((self.s, self.m), (self.m, self.s, self.base)): + args = sum((['-p', p] for p in parents), []) + q = self.git('commit-tree', self.t, *args, '-m', 'TEST-ONLY parents') + with self.assertRaisesRegex(ValueError, 'parents'): self.verify(dict(self.roles, Q=q)) + with self.assertRaisesRegex(ValueError, 'T differs'): self.verify(dict(self.roles, T=self.git('rev-parse', self.s+'^{tree}'))) + + def test_bindings_and_source_map(self): + for kind in ('missing', 'extra', 'changed'): + r = copy.deepcopy(self.roles); b = r['blob_bindings']; p = next(iter(b)) + if kind == 'missing': del b[p] + elif kind == 'extra': b['extra'] = '0'*40 + else: b[p] = '0'*40 + with self.subTest(kind=kind), self.assertRaises(ValueError): self.verify(r) + self.sources['project.godot'] = '0'*64 + with self.assertRaisesRegex(ValueError, 'SHA256'): self.verify() + + def test_outside_scope(self): + self.allowed.remove('scripts/source-only.txt') + with self.assertRaisesRegex(ValueError, 'outside'): self.verify() + + def test_fifth_conflict(self): + self.git('checkout', '-q', self.m); self.write('scripts/both.txt', 'TEST-ONLY M change\n'); m = self.commit('new M') + self.git('checkout', '-q', self.s); self.write('scripts/both.txt', 'TEST-ONLY S change\n'); s = self.commit('new S') + with self.assertRaisesRegex(ValueError, 'conflict set'): c.deterministic_tree(self.root, m, s, self.pins) + + def test_new_main_old_q_and_stale_source_new_q(self): + self.git('checkout', '-q', self.m); self.write('scenes/main/main.gd', 'TEST-ONLY changed M hunk\n'); m = self.commit('changed Main') + self.git('checkout', '-q', self.s) + with self.assertRaisesRegex(ValueError, 'parents'): self.verify(dict(self.roles, M=m)) + q = self.git('commit-tree', self.t, '-p', m, '-p', self.s, '-m', 'TEST-ONLY fresh parents stale S resolution') + with self.assertRaisesRegex(ValueError, 'new reconciliation'): self.verify(dict(self.roles, M=m, Q=q)) + + def test_docs_only_main_fresh_q(self): + self.git('checkout', '-q', self.m); self.write('docs/main-only.txt', 'TEST-ONLY new docs\n'); m = self.commit('docs Main') + self.git('checkout', '-q', self.s) + with self.assertRaisesRegex(ValueError, 'parents'): self.verify(dict(self.roles, M=m)) + # No new reconciliation needed for identical four inputs. + self.assertRegex(c.deterministic_tree(self.root, m, self.s, self.pins), '^[0-9a-f]{40}$') + + def test_final_mode_clean_exact_parents(self): + self.git('checkout', '-q', self.q) + self.verify(dict(self.roles, mode='integration_final', C=self.q)) + self.write('untracked.txt', 'TEST-ONLY dirty') + with self.assertRaisesRegex(ValueError, 'dirty'): + self.verify(dict(self.roles, mode='integration_final', C=self.q)) + + def test_root_context_sha_symlink_and_cwd(self): + p = self.root/'root-composition.json'; p.write_text(json.dumps(self.roles)) + idx = {'root_composition_sha256': hashlib.sha256(p.read_bytes()).hexdigest()} + self.assertEqual(c.root_context(self.root, idx), self.roles) + with self.assertRaisesRegex(ValueError, 'SHA'): c.root_context(self.root, {}) + p.rename(self.root/'other.json'); p.symlink_to('other.json') + with self.assertRaisesRegex(ValueError, 'symlink'): c.root_context(self.root, idx) + with self.assertRaisesRegex(ValueError, 'cwd'): c.root_identity(self.root, ROOT) + + def test_git_environment_and_crosschecks(self): + with patch.dict(os.environ, {'GIT_DIR': str(ROOT/'.git')}): + with self.assertRaises(ValueError): c.root_identity(self.root, self.root) + # An index override cannot legitimately define HEAD; it must never affect + # composition or the clean Git reads even when inherited. + with patch.dict(os.environ, {'GIT_INDEX_FILE': str(self.root/'fake-index')}): + self.assertEqual(c.root_identity(self.root, self.root), self.root) + self.verify() + with patch.dict(os.environ, {'TARGET_HEAD': '0'*40}): + with self.assertRaises(ValueError): c.root_identity(self.root, self.root) + +class CorrespondenceTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(dir=os.environ['TMPDIR'], prefix='TEST-ONLY-records-'); self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + self.roles = {'S': '1'*40, 'T': '2'*40} + self.data = {'source_hashes': {'TEST-ONLY': '3'*64}, 'os_environment': {'pid': 1234, 'session_id': 'TEST-ONLY', 'receipt_artifact_id': 'receipt', 'platform': 'macOS'}, + 'engine': {'path': str(self.root/'TEST-ONLY-engine'), 'sha256': '4'*64, 'version': 'TEST-ONLY'}, 'conditions': {}} + self.prov = dict(root=str(self.root), S='1'*40, T='2'*40, source_map_sha256=e.canonical_sha(self.data['source_hashes']), + pid=1234, session_id='TEST-ONLY', engine=self.data['engine']) + + def test_observer_payloads_and_pointer(self): + # Actual observer format, scoped structural controls only. It cannot be + # promoted to OS proof: fixture_scope remains explicitly TEST-ONLY. + doc = {'fixture_scope': 'TEST-ONLY', 'bubble_history': [dict(actor_id='worker', kind='zzz', source_state=1, recorded_at=10)], + 'modal_records': [dict(dialog_name='DefenseManager', action='open', details={'focus': 'button'}, recorded_at=11)]} + self.assertEqual(e.pointer(doc, '/bubble_history/0')['source_state'], 1) + self.assertEqual(e.pointer(doc, '/modal_records/0')['action'], 'open') + self.assertFalse(e.substantive({'pass': True, 'all': True})) + with self.assertRaises(ValueError): e.pointer(doc, '/bubble_history/00') + + def test_engine_requires_actual_receipt_and_bytes(self): + engine_file = self.root/'TEST-ONLY-engine'; engine_file.write_bytes(b'TEST-ONLY not executable') + eng = dict(path=str(engine_file), sha256=hashlib.sha256(engine_file.read_bytes()).hexdigest(), version='TEST-ONLY') + receipt = dict(engine=eng, engine_started=True, engine_ended=True, started_monotonic_ns=1, ended_monotonic_ns=2) + e.verify_engine(eng, receipt) # fingerprint structure only, never execution + for key in ('engine_started', 'engine_ended'): + with self.assertRaises(ValueError): e.verify_engine(eng, dict(receipt, **{key: False})) + with self.assertRaises(ValueError): e.verify_engine(dict(eng, sha256='0'*64), receipt) + +class CoverageAttackTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(dir=os.environ['TMPDIR'], prefix='TEST-ONLY-coverage-') + self.addCleanup(self.tmp.cleanup); self.root = Path(self.tmp.name) + engine = self.root/'engine'; engine.write_bytes(b'TEST-ONLY engine fingerprint fixture, never executed') + self.engine = dict(path=str(engine), sha256=hashlib.sha256(engine.read_bytes()).hexdigest(), version='TEST-ONLY') + self.roles = dict(S='1'*40, T='2'*40) + self.data = dict(fixture_scope='TEST-ONLY', source_hashes={'TEST-ONLY': '3'*64}, inputs={'TEST-ONLY': '4'*64}, + engine=self.engine, os_environment=dict(pid=1234, session_id='TEST-ONLY', receipt_artifact_id='receipt', platform='macOS'), conditions={}) + self.prov = dict(root=str(self.root), **self.roles, source_map_sha256=e.canonical_sha(self.data['source_hashes']), + pid=1234, session_id='TEST-ONLY', engine=self.engine) + self.receipt = dict(engine=self.engine, engine_started=True, engine_ended=True, started_monotonic_ns=1, ended_monotonic_ns=1000) + self.docs = {'receipt': dict(self.receipt, fixture_scope='TEST-ONLY', provenance=self.prov, session_id='TEST-ONLY'), + 'trace': dict(fixture_scope='TEST-ONLY', provenance=self.prov, events=[])} + events = self.docs['trace']['events'] + for cond, meanings in e.MEANINGS.items(): + coverage = {} + self.data['conditions'][cond] = dict(artifact_ids=['trace'], coverage=coverage) + for meaning, fields in meanings.items(): + row = {k: 'TEST-ONLY observed '+k for k in fields} + row.update(action=meaning, monotonic_ns=len(events)+2, provenance=self.prov) + if 'before' in fields: + row['before'] = dict(model={'value': 1}, ledger=[1], selection='worker', pending_commands=[1], save_identity='slot') + row['after'] = copy.deepcopy(row['before']) + if cond == 'unit_bubbles': + kind = {'zzz_state_model_render': 'zzz', 'anger_state_model_render': 'anger', 'heart_state_model_render': 'heart', 'switching_removal': ''}[meaning] + row['observer_record'] = dict(actor_id=row['actor_id'], kind=kind, source_state=1, recorded_at=2) + row['render'] = dict(actor_id=row['actor_id'], kind=kind) + if meaning.startswith('natural_'): + row['observer_record'] = dict(kind=meaning.removeprefix('natural_').removesuffix('_start'), heading=row['heading'], message=row['message'], recorded_at=2) + row['production_trigger'] = {'TEST-ONLY': 'synthetic fixture'} + row['domain_event'] = dict(event_id='TEST-ONLY', event_type='TEST-ONLY') + if meaning == 'valid_commit_once': row['command_ids'] = ['TEST-ONLY one'] + if meaning == 'macos_windows_classification': row.update(windows_status='NOTRUN', platform='macOS') + if meaning == 'cross_process_restart': + for n, pid in [('producer', 1234), ('consumer', 2345)]: + row[n] = dict(self.receipt, **self.roles, pid=pid, source_map_sha256=self.prov['source_map_sha256'], + manifest_sha256=row['manifest_sha256'], world_sha256=row['world_sha256']) + if meaning == 'exact_final_full_roster': row.update(results=[self.receipt], source_hashes=self.data['source_hashes'], inputs=self.data['inputs']) + coverage[meaning] = dict(artifact_id='trace', pointer='/events/'+str(len(events))) + events.append(row) + + def verify(self, actual=False): + artifacts = {} + for aid, doc in self.docs.items(): + path = self.root/(aid+'.json'); path.write_text(json.dumps(doc)) + artifacts[aid] = dict(file=path) + return e.verify(self.data, artifacts, self.roles, self.root, test_only=not actual) + + def test_one_shared_trace_distinct_records_positive(self): + self.verify() + # TEST-ONLY data is rejected by the actual entry point. + with self.assertRaisesRegex(ValueError, 'fixture/template'): self.verify(actual=True) + + def test_old_catchall_plus_seven_fillers(self): + for i in range(7): self.docs['filler'+str(i)] = dict(fixture_scope='TEST-ONLY', provenance=self.prov, pid=100+i) + with self.assertRaisesRegex(ValueError, 'filler'): self.verify() + + def test_alias_identical_bytes(self): + self.docs['alias'] = copy.deepcopy(self.docs['trace']) + with self.assertRaisesRegex(ValueError, 'alias'): self.verify() + + def test_missing_meaning_reused_and_relabelled_record(self): + coverage = self.data['conditions']['placement_and_invalid_drop']['coverage'] + removed = coverage.pop('grid_snap') + with self.assertRaisesRegex(ValueError, 'meaning'): self.verify() + coverage['grid_snap'] = coverage['connection_warning'] + with self.assertRaisesRegex(ValueError, 'meaning|reused'): self.verify() + coverage['grid_snap'] = removed + self.docs['trace']['events'][0]['action'] = 'other' + with self.assertRaisesRegex(ValueError, 'meaning'): self.verify() + + def test_wrong_root_source_and_engine(self): + original = copy.deepcopy(self.prov) + for key in ('root', 'S', 'T', 'source_map_sha256', 'pid', 'session_id', 'engine'): + self.docs['trace']['provenance'] = dict(original, **{key: 'wrong'}) + with self.subTest(key=key), self.assertRaisesRegex(ValueError, 'mismatch'): self.verify() + self.docs['trace']['provenance'] = original + self.docs['trace']['events'][0]['provenance'] = dict(original, S='0'*40) + with self.assertRaisesRegex(ValueError, 'record provenance'): self.verify() + + def test_all_true_opaque_and_scaffold(self): + row = self.docs['trace']['events'][0] + for k in e.MEANINGS['placement_and_invalid_drop']['grid_snap']: row[k] = {'passed': True} + with self.assertRaisesRegex(ValueError, 'opaque'): self.verify() + self.docs['trace']['fixture_scope'] = 'pending-template' + with self.assertRaisesRegex(ValueError, 'fixture/template'): self.verify() + + def test_cross_process_and_whole_roster_engine(self): + event = next(r for r in self.docs['trace']['events'] if r['action'] == 'cross_process_restart') + event['consumer']['pid'] = event['producer']['pid'] + with self.assertRaisesRegex(ValueError, 'separate process'): self.verify() + event['consumer']['pid'] = 2345 + event = next(r for r in self.docs['trace']['events'] if r['action'] == 'exact_final_full_roster') + event['results'][0] = dict(self.receipt, engine=dict(self.engine, version='other')) + with self.assertRaisesRegex(ValueError, 'engine mismatch'): self.verify() + +class PreservationTests(unittest.TestCase): + def test_exact_nonp24_and_schema_preservation(self): + test = 'tests/ui/test_operations_notification.gd' + digest = hashlib.sha256((ROOT/test).read_bytes()).hexdigest() + for path, container, field in ((c.CONTRACT, 'rows', 'source_sha256_at_schema_audit'), ('tools/regression/suite-policy.json', 'suite_policies', 'source_sha256')): + old = json.loads(c.git_bytes(ROOT, 'show', 'bd3a13405115fc9bf6c5acec55bcfef7dd15266a:'+path)) + new = json.loads((ROOT/path).read_bytes()) + rows = new[container] + row = next(r for r in rows if r['test'] == test) if isinstance(rows, list) else rows[test] + self.assertEqual(row[field], digest) + old_rows = old[container] + old_row = next(r for r in old_rows if r['test'] == test) if isinstance(old_rows, list) else old_rows[test] + row[field] = old_row[field] + self.assertEqual(new, old) + self.assertEqual((ROOT/'scenes/main/world_preview.gd').read_bytes(), c.git_bytes(ROOT, 'show', 'HEAD:scenes/main/world_preview.gd')) + old = c.git_bytes(ROOT, 'show', 'HEAD:'+test).decode() + new = (ROOT/test).read_text() + self.assertIn('await FixtureCleanup.free_owner_and_wait(self, main)', new) + self.assertEqual(old[old.index('\tvar cleanup:'):], new[new.index('\tvar cleanup:'):]) + start = new.index('\t# Defense dialog uses the ordinary visibility callback') + end = new.index('\t# Pause freezes simulation via process_real_delta', start) + self.assertEqual(new[:start]+new[end:], old) + main_old = c.git_bytes(ROOT, 'show', 'HEAD:scenes/main/main.gd').decode() + self.assertEqual((ROOT/'scenes/main/main.gd').read_text(), main_old.replace( + '\t\tadd_child(defense_manager)', + '\t\tdefense_manager.visibility_changed.connect(_update_modal_input_guard)\n\t\tadd_child(defense_manager)')) + for path, oid in c.MAIN_INPUTS.items(): + self.assertEqual(c.tree(ROOT, c.RECONCILED_MAIN)[path][2], oid) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/scenes/main/main.gd b/scenes/main/main.gd index a3bba86..5d13619 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -3085,6 +3085,7 @@ func _ensure_defense_manager() -> DefenseManagementPanel: panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL defense_manager.add_child(scroll) scroll.add_child(panel) + defense_manager.visibility_changed.connect(_update_modal_input_guard) add_child(defense_manager) return defense_manager.find_child("DefenseManagementPanel", true, false) as DefenseManagementPanel diff --git a/tests/ui/test_operations_notification.gd b/tests/ui/test_operations_notification.gd index b0df93d..15dc9e6 100644 --- a/tests/ui/test_operations_notification.gd +++ b/tests/ui/test_operations_notification.gd @@ -133,6 +133,31 @@ func _run() -> void: _expect(not preview.modal_input_blocked, "Modal close unblocks behind-dialog input") _expect(main.manual_save_path == initial_manual_path, "Modal cancellation preserves existing save identity") + # Defense dialog uses the ordinary visibility callback and real model. + main.call(&"_show_defense_manager") + await process_frame + _expect(preview.modal_input_blocked, "Defense dialog blocks map input on open") + preview.set_build_definition(reception_def, 0) + var defense_before: Dictionary = runtime.create_snapshot() + var defense_selection := [preview.selected_entity_id, staff_panel.selected_staff_id, + main.get("_selected_entity_id"), main.get("_selected_entity_kind")] + var behind_click := InputEventMouseButton.new() + behind_click.button_index = MOUSE_BUTTON_LEFT + behind_click.pressed = true + behind_click.position = Vector2(200, 200) + preview._gui_input(behind_click) + preview._drop_data(Vector2.ZERO, drop_payload) + _expect(SimulationDigest.compute(defense_before) == SimulationDigest.compute(runtime.create_snapshot()), + "Defense modal build/drop attempts preserve complete model including economy ledger") + _expect(defense_selection == [preview.selected_entity_id, staff_panel.selected_staff_id, + main.get("_selected_entity_id"), main.get("_selected_entity_kind")], + "Defense modal build/drop attempts preserve map and staff selections") + preview.clear_build_definition() + var defense_dialog: AcceptDialog = main.get("defense_manager") + defense_dialog.hide() + await process_frame + _expect(not preview.modal_input_blocked, "Defense dialog close unblocks map input") + # Pause freezes simulation via process_real_delta/accumulator; ordinary resume continues exactly once var clock_paused_before: bool = bool(clock.get("paused")) clock.set_paused(true) diff --git a/tools/check_p24_operations_evidence.py b/tools/check_p24_operations_evidence.py index 56d2fc0..57a1b34 100644 --- a/tools/check_p24_operations_evidence.py +++ b/tools/check_p24_operations_evidence.py @@ -20,8 +20,20 @@ import json import os import re import sys +import importlib.util from pathlib import Path +# This helper ships in S; it has only stdlib dependencies (no Main-only imports). +_HELPER = Path(__file__).resolve().parents[1] / 'docs/plans/original-completion-2026-10-02/execution/p24-02/source-ready-repair5-20261017/composition.py' +_spec = importlib.util.spec_from_file_location('p24_composition', _HELPER) +composition = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(composition) +_spec = importlib.util.spec_from_file_location('p24_correspondence', _HELPER.with_name('correspondence.py')) +correspondence = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(correspondence) +ROOT = Path(composition.git_bytes(Path(__file__).resolve().parents[1], 'rev-parse', '--show-toplevel').decode().strip()) +VERIFIED_ROOT = ROOT + REQUIRED_CONDITIONS = [ "placement_and_invalid_drop", "modal_and_pause_resume", @@ -66,21 +78,12 @@ def compute_sha256(path: Path) -> str: def get_actual_git_head(root: Path = None) -> str: - root = root if root is not None else Path(__file__).resolve().parents[1] + root = root if root is not None else ROOT try: - import subprocess - res = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root, capture_output=True, text=True, timeout=5) - if res.returncode == 0: - h = res.stdout.strip().lower() - if HEX_COMMIT_PATTERN.match(h) and not (set(h) <= {"0"} or set(h) <= {"f"}): - for name in ("CANDIDATE_COMMIT", "TARGET_HEAD"): - if name in os.environ and os.environ[name].strip().lower() != h: - print(f"ERROR: {name} mismatch with real git HEAD {h}", file=sys.stderr) - return "" - return h + composition.root_identity(root, root) + return composition.git_bytes(root, 'rev-parse', 'HEAD').decode().strip() except Exception: - pass - return "" + return "" def get_required_suite_roster(root: Path) -> list[str]: @@ -93,7 +96,7 @@ def get_required_suite_roster(root: Path) -> list[str]: def find_index_file(custom_path: str = None) -> Path: if custom_path: - p = Path(custom_path) + p = Path(custom_path) if Path(custom_path).is_absolute() else ROOT / custom_path if p.is_file(): return p.resolve() if p.is_dir(): @@ -114,9 +117,9 @@ def find_index_file(custom_path: str = None) -> Path: return candidate2.resolve() candidates = [ - Path(".runtime/P24-02/build/p24-operations-evidence/index.json"), - Path("docs/plans/original-completion-2026-10-02/execution/p24-02/evidence/index.json"), - Path("artifacts/p24-02-operations/index.json"), + ROOT / Path(".runtime/P24-02/build/p24-operations-evidence/index.json"), + ROOT / Path("docs/plans/original-completion-2026-10-02/execution/p24-02/evidence/index.json"), + ROOT / Path("artifacts/p24-02-operations/index.json"), ] for c in candidates: if c.is_file(): @@ -368,7 +371,7 @@ def _validate_condition_artifact_content( if not full_roster or not isinstance(full_roster, dict): return False, "Missing 'full_roster' summary in final validation review evidence" - repo_root = Path(".").resolve() + repo_root = VERIFIED_ROOT required_suites = get_required_suite_roster(repo_root) total_required = len(required_suites) @@ -428,7 +431,7 @@ def _validate_condition_artifact_content( return True, "" -def verify_evidence(index_path: Path) -> bool: +def _verify_evidence(index_path: Path) -> bool: print(f"Reading evidence index from: {index_path}") try: data = json.loads(index_path.read_text(encoding="utf-8")) @@ -487,13 +490,13 @@ def verify_evidence(index_path: Path) -> bool: print("ERROR: 'source_hashes' dictionary is required in evidence index", file=sys.stderr) return False - missing_scope_sources = [f for f in REQUIRED_SCOPE_FILES if f not in sources] + missing_scope_sources = [f for f in REQUIRED_SCOPE_FILES if f not in sources and f not in data.get("inputs", {})] if missing_scope_sources: print(f"ERROR: 'source_hashes' omits required scope files: {missing_scope_sources}", file=sys.stderr) return False for rel_path, expected_hash in sources.items(): - p = Path(rel_path) + p = composition.safe_path(VERIFIED_ROOT, rel_path) if not p.is_file(): print(f"ERROR: Required source file does not exist on disk: {rel_path}", file=sys.stderr) return False @@ -530,9 +533,9 @@ def verify_evidence(index_path: Path) -> bool: print(f"ERROR: Artifact '{art_id}' path traversal or absolute path forbidden: {rel_art_path}", file=sys.stderr) return False - art_file = evidence_dir / rel_art_path + art_file = composition.safe_path(evidence_dir, rel_art_path) if not art_file.is_file(): - art_file = Path(rel_art_path) + art_file = composition.safe_path(ROOT, rel_art_path) if not art_file.is_file(): print(f"ERROR: Referenced artifact file missing on disk: {rel_art_path}", file=sys.stderr) return False @@ -712,25 +715,48 @@ def verify_evidence(index_path: Path) -> bool: print(f"ERROR: Condition '{cond}' artifact content verification failed: {last_err}", file=sys.stderr) return False - # Reject self-declaration where conditions share a single generic artifact - unique_referenced_artifacts = set() - for aids in condition_to_artifacts.values(): - unique_referenced_artifacts.update(aids) - - if len(unique_referenced_artifacts) < len(REQUIRED_CONDITIONS): - print(f"ERROR: All 8 conditions require distinct evidence artifacts; found only {len(unique_referenced_artifacts)} unique artifacts across {len(REQUIRED_CONDITIONS)} conditions", file=sys.stderr) + try: + correspondence.verify(data, validated_artifacts, composition.root_context(evidence_dir, data), ROOT) + except (ValueError, KeyError, TypeError, OSError, IndexError) as exc: + print(f"ERROR: correspondence: {exc}", file=sys.stderr) return False print("SUCCESS: All 8 required conditions, candidate version, scope manifest, and OS execution artifacts strictly verified.") return True +def verify_evidence(index_path: Path) -> bool: + global VERIFIED_ROOT + try: + composition.root_identity(ROOT, Path.cwd()) + data = composition.strict_json(index_path.read_bytes()) + roles = composition.root_context(index_path.parent, data) + composition.require(data.get('inputs', {}).get(composition.SPEC) == EXPECTED_SPEC_SHA256, 'frozen spec input SHA mismatch') + required_helpers = {_HELPER.relative_to(ROOT).as_posix(), _HELPER.with_name('correspondence.py').relative_to(ROOT).as_posix()} + composition.require(required_helpers <= set(data['inputs']), 'missing checker helper input pins') + composition.verify_composition(ROOT, roles, data.get('source_hashes'), data.get('inputs'), + EXPECTED_BASE_COMMIT, REQUIRED_SCOPE_FILES + ['docs/plans/original-completion-2026-10-02/execution/p24-02/**']) + with composition.tested_checkout(ROOT, roles['T']) as view: + VERIFIED_ROOT = view + return _verify_evidence(index_path) + except (ValueError, KeyError, TypeError, OSError, IndexError) as exc: + print(f"ERROR: composition: {exc}", file=sys.stderr) + return False + finally: + VERIFIED_ROOT = ROOT + + def main() -> int: parser = argparse.ArgumentParser(description="Check P24-02 Operations Evidence") parser.add_argument("--verify-only", action="store_true", help="Strict verification of existing evidence index only") parser.add_argument("--index", type=str, help="Path to evidence index.json", default=None) args = parser.parse_args() + try: + composition.root_identity(ROOT, Path.cwd()) + except (ValueError, OSError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 index_file = find_index_file(args.index) if not index_file: print("ERROR: Could not locate p24-operations-evidence index.json.", file=sys.stderr) diff --git a/tools/regression/suite-policy.json b/tools/regression/suite-policy.json index de0ab07..d0bddce 100644 --- a/tools/regression/suite-policy.json +++ b/tools/regression/suite-policy.json @@ -416,7 +416,7 @@ "new_audio_QA": false }, "tests/ui/test_operations_notification.gd": { - "source_sha256": "9c26386c529b8c8135639e8d0f0ed390da7139d64ff1e268975b05948cab9147", + "source_sha256": "50172ab6daa031d221f6a78cac5cdbcb8884f295f8062b54656562e2412faff2", "ordinary_cleanup_prepared": true, "ordinary_control_labels": ["single"], "old_bounded_teardown": false,