# P16-01-DISMISS 実装の記録(実 Opus の実装担当、2026-10-24、修正 1 は 2026-10-25) - 仕様: 同じフォルダの `SPEC.md`(登録版 r04。main `0b59d7c` に固定。ファイルの SHA-256 は `b4cc31c0…5f77`、Runner の `spec_sha256` は `14efffd9…e6b`)。出典は `PROVENANCE.json`。 - 原本への加算は 0。原単位 P16-01・P16-03、`AR.staff.dismissal_severance` の package P05 の原 work・原 AC は閉じない(SPEC §8)。 - 管理の記録は `artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/` にある(git の外)。 - 変えた製品のファイル: `autoload/simulation_runtime.gd`・`scripts/economy/finance_policy.gd`・`scripts/ui/staff_management_panel.gd`。`scripts/staff/staff_service.gd` と `scenes/main/main.gd` は変えていない(§1・§4)。 ## 0. 修正 1 の経過(S `b06fec1` → 修正の後の S) - S `b06fec1` は、同じ実 reviewer の補足(改訂)の判定で changes_requested になった(high 1・medium 6・low 4。`DESKTOP_RESULT_SUPPLEMENT_R02.json`)。1回目の pass と、その後の補足の判定は、どちらも書き換えずに残している。 - 主な所見と直し: - high: S `b06fec1` は、乗ったままの救護の組の解雇を、実装の判断で `RESCUE_PAIR_ABOARD` として拒んでいた。これは SPEC §6.2 の最後の文「直し、拒否に変えない」、F1・F7(許可の全ての行)に合わない。拒否を消し、組を解く後始末を作った(§3)。 - medium: 開き直した予約つきの作業を別の職員が取れなかった(`IDEMPOTENCY_CONFLICT` で BLOCKED)。予約の操作 ID を直した(§5)。 - medium: preview と結果が、命令の中で取り消す救護の搬送を「開き直す」と書いていた。分けて実際どおりにした(§4)。 - medium: preview が R5(運営の終了)と R3(予約を解けない)を判定していなかった。判定を足した(§1)。 - medium と low: 条文が明示する確かめ方を代用していた所がある(同じ process の読み込み、通勤の昇降機、拒否の行の画面、bytes の比べ、別の process の境目など)。条文どおりの試験を足した(§7)。 - 新しい拒否や、R9 に似た境界は足していない。受入の条件も削っていない。 ## 1. 命令と判定の順(SPEC §4.1) - `SimulationRuntime` に通常の command `dismiss_staff`(payload `staff_id`)を足した(`_execute_dismiss_staff`)。 - (a) 運営の終了(`BANKRUPT`)と (b) 同じ key の再送(`DUPLICATE_IDEMPOTENCY_KEY`)は、今の `submit_command` と `CommandQueue.enqueue` のまま受付の前・受付で止まる(R5・R6、5.b)。 - (c) executor は、状態を変える前に次の順で確かめる。どれかに当たれば、その理由で失敗の結果を返す(5.c)。 1. payload に `staff_id` が無い → `INVALID_PAYLOAD` 2. 職員が居ない → `NOT_FOUND`(R4) 3. `UNIQUE` → `UNIQUE`(R1。今の `remove_staff` と同じ code と文) 4. `FREE_HIRE` でない(`ADULT_CONTRACT`)→ `NOT_GENERAL_EMPLOYEE`(R7) 5. 外部が持つ作業(仕入れの物流の受け取り・配達・返送)→ `EXTERNAL_TASK`(R2。今の `remove_staff` と同じ code と文) 6. 持っている作業の予約を解けない → `RESERVATION_RELEASE_FAILED`(R3。今の `remove_staff` と同じ code と文) - 修正 1 で、変える前の判定にした(`_staff_task_reservations_releasable`)。在庫の解放は、予約が無い時と消費済みの時に失敗する(`InventoryService.release_reservation`)。それを、読むだけの `get_reservation` で前もって見る。 - `remove_staff` の同じ拒否は、最後の守りとして残す。そこで止まった時も、全体を巻き戻す。 7. 終わっていない襲撃の防衛者 → `DEFENSE_LOCKED`(R9。Root の一時の境界) 8. 表金と裏金の合計が退職金に足りない → `INSUFFICIENT_FUNDS`(R8) - (d) 全て通れば、次を1つの操作として行う。 1. 乗ったままの救護の組なら、解雇する人の座席を解く(§3)。 2. 既存の削除の契約 `StaffService.remove_staff(staff_id, &"DISMISSED", true, 分)`。 3. 退職金の記帳(§2)。 4. 救護の後始末(§3)、施設の人員の投影(`_sync_facility_staffing`)、出来事の欠けた職員の扱い(`event_service.audit_actor_availability`)。 - (d) の途中で失敗すれば、命令の前の写し(staff・inventory・path・elevators・移動の残り・出来事・ID・帳簿・Gold の写し)に全て戻して、`TRANSACTION_APPLY_FAILED` を返す。理由は次のとおり。 - 座席を解けない時 `RESCUE_SEAT_RELEASE_FAILED` - `remove_staff` が予約を解けない時 `RESERVATION_RELEASE_FAILED`(R3 の最後の守り) - 退職金を記帳できない時 `SEVERANCE_POST_FAILED` - 救護の作業を取り消せない時 `RESCUE_CLEANUP_FAILED` - 退職金と救護の2つは、試験で失敗を起こし、5.c の比べ方で何も変わらないことを確かめる。座席と `remove_staff` の2つは、試験で失敗を起こしていない(前もった判定と公開の照合の形で、今の試験の組み立てからは起こせない守り)。 - 画面と試験のための読むだけの関数 `get_staff_dismissal_preview(staff_id)`。返すのは、解雇できるか・理由・退職金・未払い・開き直す作業・命令の中で取り消す救護の作業と役(運搬者か患者か)・表金・資金の合計。 - 運営の終了の後は、理由を `BANKRUPT` にする(R5。命令は受付の前に拒まれる)。 - `get_staff_management_report` の各行に `dismissal` として載る。 ## 2. 退職金(SPEC §3.5・§3.6、A14) - `FinancePolicy` に、`SEVERANCE_EXPENSE`(口座 `STAFF_SEVERANCE_EXPENSE`)・`SEVERANCE_CATEGORY`(区分 `STAFF_SEVERANCE`)・`SEVERANCE_DAYS`(3)・`severance(日給)` を足した。額は `get_effective_daily_wage() × 3`。 - 記帳は命令の時刻に、1つの取引(idempotency key `staff.severance.`)で行う。 - 表金で足りない分: 裏金 → 表金の補填(区分 `BACK_FUND_TOP_UP`。P05-02 の返済と同じ形)。 - 退職金: 表金 → `STAFF_SEVERANCE_EXPENSE`(区分 `STAFF_SEVERANCE`、source は職員の ID)。 - 日次決算の照合では、`cost_of_goods`(画面「運営費(仕入れ・契約金等)」)に入れる。 - 写した表は `_build_operations_daily_financials` と `_operations_expected_category_totals` の2つ。施設ごとの表は、施設でない source を読まないので変えていない。 - 補填は、fund report v2 の `back_top_up`(その日の全ての補填)に入る。 - 旧セーブ: `FinancePolicy.prepare_accounts` の最後で、退職金の口座が無ければ空の口座として足す。A12 の5つの口座とは別に判定するので、P05-02 の後・P16 の前のセーブにも足す。口座があって負を許す設定なら拒む。 - 解雇の前に貯まった未払いの給与は、今の契約のまま `_departed_payroll` に保持し、次の 00:00 の締めで払う(A09 の補填を含む)。解雇の日の日給は、今の給与の契約のまま(締めの時に雇用中の職員だけ)。 ## 3. 後始末(SPEC §6.2・§6.2.1) - 勤務・予定・配置は `remove_staff` が行う。経路の actor・昇降機の予約・移動の残り・`STAFF_REMOVED`(reason `DISMISSED`)は、今の `_on_staff_removed` が行う。 - 施設の人員の投影は、命令の中で `_sync_facility_staffing` をすぐ行う。サービスの途中の職員が抜けて人数が `staff_min` を欠けると、次の施設の更新から今の規則で職員を待ち(5 分)、代わりが来なければ失敗する(A4 の試験)。 - **乗ったままの救護の組(修正 1)**: S `b06fec1` の `RESCUE_PAIR_ABOARD` の拒否を消した。組の段階でも解雇できる。 - 解雇する人の座席だけを、命令の時に組から解く(`_release_dismissed_rescue_seat`)。SPEC §10 の「非描写の状態として命令の時点で取り除く」に当たる。 - 手順は、`ElevatorService` 自身の乗っている1人の取り消し(`_cancel_single`)と同じ。公開の `path_service.complete_elevator_request` で乗車の要求を終え、昇降機の公開の snapshot から、その人の trip・座席・相方の `companion_id` を外して `restore_snapshot` に渡す。 - `ElevatorService` は変えていない。`restore_snapshot` の照合(Path の要求と trip の対応、座席、相方の対応)を通る形なので、命令の直後の保存も照合を通る。 - 残る人(患者または運搬者)は、今の規則(`_advance_staff_rescue` の、乗っている間は組を保つ分岐)どおり、1人で乗ったまま本物の階に着く。 - 運搬者を解雇した時: 患者は着いた階で倒れたままになり、次の時刻の救護の確認が、迎えを今の規則で作り直す。 - 患者を解雇した時: 運搬者は着いた階で手が空く。 - 試験は組の段階で運搬者と患者のそれぞれを解雇し、次を確かめる(§7)。 - 参照が残らない。 - 残る人が本物の階(昇降機の止まる階)で降りる。 - 救護が今の規則でやり直される。 - 命令の直後の保存を、同じ process(写しの復元・ディスク)と別の process(再開の2段)で読む。 - 救護(`_interrupt_dismissed_staff_rescue`): 次の時刻の救護の確認(`_advance_staff_rescue` の最初の段)と同じ規則を、命令の中で、解雇した職員に関わる作業にだけ当てる。 - 患者を解雇した時: その患者を目標にした、終わっていない救護の作業を `RESCUE_INTERRUPTED` で取り消し、運搬者を作業から外す。 - 運搬者を解雇した時: `remove_staff` が開き直した搬送(`rescue_transport`)は、運搬者が居ないので取り消す(今の規則と同じ)。迎え(`rescue_pickup`)は開き直ったまま、別の医療職を待つ(今の規則でも有効な作業)。 - 昇降機の予約の取り消しは、乗っていない生きた人だけに行う。乗っている人は、着くまで乗せたままにする(今の規則)。 - 出来事: 命令の中で、公開の `event_service.audit_actor_availability` を呼ぶ。進行中・選択待ち・中断の出来事の欠けた職員を、今の `missing_actor_policy` で扱う(CANCEL と CONTINUE_WITHOUT を試験)。予約した費用は、今の規則で1回だけ戻る。この audit は他の欠けた actor も今の規則で扱う(次の時刻と同じ処理が早く行われるだけ)。 - 命令の直後に保存し、同じ process と別の process で読み込んでも、欠けた職員への生きた参照が残らず、保存の照合が通る。生きた参照とは、終わっていない作業の患者と運搬者、昇降機の予約と相方、経路の actor、施設の人員、出来事の拘束と actor のこと。 - 履歴として残るもの: 終わった作業の記録の `target_entity_id`、サービスの `staff_ids`(始めた時の記録)、給与の締めの行、雇った候補の記録(同じ候補は二度雇えない今の規則のまま)。 ## 4. 画面(SPEC §4.3) - `StaffManagementPanel` に、「解雇する…」と確定の欄(「解雇を確定」「やめる」)を足した。判定は Runtime の preview に従う。 - 押す前に、退職金の額(日給の3日分)、裏金からの補填、未払いの給与、開き直す作業を出す。 - 修正 1 で、命令の中で取り消す救護を分けて出す(`interrupted_rescue_roles`)。運搬者なら「運んでいる救護の搬送は取り消し、患者の救護をやり直します。」、患者なら「この従業員への救護は取り消し、救護係を外します。」。 - 拒否の行(R1・R2・R3・R5・R7・R8・R9)ではボタンを無効にし、理由を出す。`UNIQUE` は今の「ユニーク:雇用・維持費無料 / 解雇・売却不可」、それ以外は「解雇不可:…」。無効のボタンを無理に押しても、命令を送らない(5.a)。 - 確定の時に名簿を読み直す。読み込みや変化の後の古い確認では送らない。 - 時間が止まっている時はすぐに、進んでいる時は次の時刻に、命令が処理される。 - 結果として、払った退職金・補填・保持した未払い・開き直した作業・取り消した救護、または拒否の理由(押した後に資金が減って拒まれた場合を含む)を出し、名簿と選択を更新する。 - 解雇の受理を待つ間は、その職員の配属の変更とドラッグを止める。 - `Main` の職員の管理の画面は、今の部品と送り手(`_audio_feedback.submit_command`)のままで使えるので、`scenes/main/main.gd` は変えていない。UI は今の部品のままで、完成の見た目ではない。 - 入力の経路(F9 の開示): 試験は本物の Main と Runtime を使う。Main のキーの処理(`_unhandled_key_input`)と、画面のボタンの `pressed` の信号で操作し、他の Main の suite と同じ形で通常の入口を通す。 - viewport や OS の入力イベントは通していない。headless の診断では、`push_input` と `Input.parse_input_event` の形を1回ずつ試したが、ボタンは反応しなかった。これは headless の診断の結果で、実機の OS 入力を試して失敗した証拠ではない。 - Windows の実機・IME・OS の倍率・同じ候補での実機の受入は、未達のまま残す。 ## 5. 予約の操作 ID(修正 1、A5・F7) - 原因: 在庫の予約の操作 ID が、作業と品目だけで決まっていた(`staff.task.reserve.<作業>.<品目>`)。在庫の照合の fingerprint には予約した分が入るので、開き直した作業を別の分に別の職員が取ると、`IDEMPOTENCY_CONFLICT` になって作業が BLOCKED になる(戻す経路は無い)。同じ分なら、閉じた旧い予約の結果が再生される。 - 直し(`_reserve_staff_task_items`): 操作 ID に、その作業のその品目のこれまでの予約の数を足す(`staff.task.reserve.<作業>.<品目>.<回数>`)。在庫は予約も照合の記録も消さないので、回数は増える一方で、予約ごとに別の操作になる。分ではなく回数にしたので、同じ分に解雇して取り直しても、旧い予約に当たらない。 - 試験(§7): - 元の予約・解雇・別の職員の再予約を同じ分で起こし、別の職員が終えるまで。同じ分の派遣は、StaffService の公開の `dispatch_now` で起こす。 - 列で処理される解雇と同じ tick での再予約から、完了まで。 - 清掃(`facility_clean`)の作業の開き直しと、別の清掃係による完了。 - 既存の試験は、この操作 ID の文字列を見ていない(期待の変更なし)。 ## 6. 見つけた既存の不具合と直し(S `b06fec1` で直したもの、変更なし) - 締めの後に税・利息の未払いが残ったゲームの保存が読めなかった(P05-02 からの不具合)。 - 保存を読む時の Operations の照合(`_parse_operations_runtime_snapshot`)は、締めた未払いを「給与 + 維持費」だけで数えていた。実行中の照合(`_operations_closed_arrears_total`)が足している税・利息の未払い(`_finance_arrears_through`)を、数えていなかった。 - 読む時の照合を、実行中と同じ式にそろえた。照合を緩めたのではない。 - 試験: 帳簿の日の流れが、税の未払い 30 を確かめてから締めの後の保存を復元する。同じ process(修正 1 で全ての保存点)と別の process でも読む。 ## 7. 試験 - 新しい suite `tests/staff/test_general_dismissal.gd`(F1〜F9。§6 の全ての行。共通の部品は `tests/helpers/p16_dismissal_fixture.gd`)。最後の場面がゲームの Main を使うので、suite-policy で ordinary の音の後片付けの証明(`single`)を持つ。修正 1 で足した場面: - 乗ったままの組を含む、救護の全ての段階での運搬者と患者の解雇。preview と結果で、開き直すと取り消すを分けて確かめる。 - 通勤の職員が上の階の職場へ向かう時に、昇降機を待つ段と乗っている段で解雇する(A2)。 - 予約つきの作業の同じ分の再予約と完了、同じ tick の再予約と完了。清掃の作業の開き直しと完了(A5・F7)。 - 帳簿の日の流れの全ての保存点を、同じ process でディスクから読み、最後まで続ける(F8)。 - 物流の返送の段の拒否(R2)。 - 救護の後始末の失敗の巻き戻し(5.c)。 - 列で待つ命令の処理を 5.c の増分で比べる(A9)。 - その命令は、Runtime 自身の命令の段(`_process_commands`)で、tick の他の処理なしに処理する。 - Main の画面で、拒否の行の全て(R1・R2・R3・R5・R7・R8・R9)を確かめる。ボタンが無効で、理由が出て、無理に押しても snapshot の全体の bytes が同じ。 - 保持した未払い(本物の 0 時の締めで資金が足りず、日給が未払いになる)も、押す前と結果で確かめる。 - 押した後の拒否の理由も確かめる。 - 画面の比べは全て `var_to_bytes`。 - 再開の2段 `tests/staff/test_general_dismissal_restart_1_write.gd`(唯一の生成ファイルは `artifacts/p16-01-dismiss/restart-handoff.json`)と `..._2_read.gd`。 - 帳簿の6つの保存点を読む。 - 次の命令の境目の保存を読む。修正 1 で、A6 の迎え・A6 と A7 の乗ったままの組・A8 の CONTINUE_WITHOUT を足した。 - A6: 搬送を徒歩で運ぶ段・迎えの段・組で乗っている段 - A7: 運搬者が1人で乗っている段・組で乗っている段 - A8: CANCEL・CONTINUE_WITHOUT - 局所の確かめで分かった、既存の規則による制約(製品の不具合ではない。試験の組み立てを直した): - 保持した未払いの給与は未払いの費用として数えられ、建設と雇用を止める(`UNPAID_EXPENSES`)。 - 締めを通らない未払いを持たせた職員の登録は、運営の照合(`OPERATIONS_CROSSLINK_MISMATCH`)を止める。そのため画面の試験は、本物の締めで未払いを作る。 - validation `p16-dismiss`: `tools/run_p16_dismiss_tests.py`。`tools/run_p07_weather_tests.py` から、名前・名簿・再開の段・handoff・protocol 名だけを明示の置き換えで変えた(段の手順・厳密な照合・ordinary と old_bounded の分類は同じ)。実 12 段(新しい suite 1 + 既存 9 + 再開 2)と import・parse。 - suite-policy と marker contract に新しい3本の行を足し、今の source の SHA-256 で固定した。 - 既存の試験の期待値の変更は `EXPECTATION_CHANGES.json`(kitchen の旧セーブの写しに退職金の口座を足す1件)。 ## 8. 範囲の外として残すもの(主張しない) - まだ始まっていない(待ち行列の)出来事の actor の ID は、拘束を持たない。始まる時に、今の規則(`start_next_event` の欠けた actor の扱い)で扱われる。命令の中では変えない。 - 進行中の襲撃の防衛者の解雇(R9)は、原本の要件として未達のまま(SPEC §6.3・§8)。 - 解雇した職員が歩いて出る描写は作らない(SPEC §10)。UI は今の部品のまま。Windows の実機・IME・OS の倍率・同じ候補の実機の受入は後。 - 原単位 P16-01・P16-03 の残りは未達のまま(SPEC §8): 求人募集、奴隷市場・破産客からの獲得、人身売買、勤務・休息・気絶・救護の横断、解雇の経路の外の退職時の参照の除去。