From ad1405c8a659eebf8f9f18930f5d8208c09b5713 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:29:22 +0800 Subject: [PATCH 01/12] task: define HSR-RUNTIME-ARCH-043 --- LUMEN_TASK_HSR_RUNTIME_ARCH_043.md | 329 +++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 LUMEN_TASK_HSR_RUNTIME_ARCH_043.md diff --git a/LUMEN_TASK_HSR_RUNTIME_ARCH_043.md b/LUMEN_TASK_HSR_RUNTIME_ARCH_043.md new file mode 100644 index 0000000..8ac53a1 --- /dev/null +++ b/LUMEN_TASK_HSR_RUNTIME_ARCH_043.md @@ -0,0 +1,329 @@ +# HSR-RUNTIME-ARCH-043 — GrantExtraTurn Runtime Observation Contract + +## Current confirmed state + +Canonical accepted `main` before this task: + +`23be13799facfe63152d727459f345f03e87fac6` + +Last confirmed post-merge validation (GitHub Actions run #257): + +- compile: PASS; +- pytest: **1687 passed in 10.16s**; +- legacy regression: **20/20**; +- trace evidence: **2/2**; +- standalone runtime action-session Golden regression: **9/9**. + +Already accepted production semantics relevant to this task: + +- `GrantExtraTurn.apply()` resolves targets through existing `UnitEffect.target_units()` and appends each resolved `unit.id` to `state.extra_turn_stack` in resolved/declaration order. +- `Timeline.next_turn()` gives queued extra turns precedence over the normal timeline and resolves the stack by `pop()` from the end, therefore the simulator's accepted deterministic ordering is LIFO. +- extra turns do not advance global AV and do not alter the original normal-timeline AV values merely by being selected. +- `RuntimeEventType.EXTRA_TURN_QUEUED` already exists in the stable runtime event vocabulary. +- no production legacy event currently observes `GrantExtraTurn` queue mutation. + +These are simulator semantics only. They are not a claim that real HSR uses this exact hidden priority/interrupt model. + +## Objective + +Make the already-existing production `GrantExtraTurn` stack append observable through the runtime trace pipeline without changing queue mutation, target resolution, LIFO resolution, normal timeline behavior, or any other simulator semantics. + +This task is observation-only. It must expose the concrete queue mutation that already occurs; it must not invent or infer game priority, interrupt, scheduling, or extra-action rules. + +## Required implementation + +### 1. Preserve exact production queue mutation + +For every target resolved by `GrantExtraTurn.target_units(state, action)`, preserve the existing append exactly: + +```python +state.extra_turn_stack.append(unit.id) +``` + +Immediately around that existing mutation, capture: + +```python +stack_depth_before = len(state.extra_turn_stack) +state.extra_turn_stack.append(unit.id) +stack_depth_after = len(state.extra_turn_stack) +``` + +Do not reorder targets, deduplicate targets, replace the list, sort the stack, or change `Timeline.next_turn()`. + +### 2. Emit one post-mutation legacy event per appended target + +After each append, emit exactly one legacy event: + +`extra_turn_queued` + +with exact data fields: + +- `actor_id` +- `action_id` +- `target_id` +- `stack_depth_before` +- `stack_depth_after` + +No priority value, queue rank, interrupt marker, turn-kind inference, AV mutation, source timestamp, hidden HSR mechanic value, or extra-action metadata may be added. + +The event must be emitted after the append, so event-trigger handlers observe the already-mutated stack. + +For a one-target non-ending action, pending-event order must be exactly: + +`action_started -> extra_turn_queued -> action_finished` + +### 3. Add a strict frozen typed runtime observation + +Add a dedicated runtime contract named: + +`RuntimeExtraTurnQueuedObservation` + +Prefer a semantically dedicated module such as: + +`hsr_axis_sim/runtime_contracts/turn_order_observations.py` + +rather than treating queue depth as an AV/action-axis observation. + +Exact fields: + +- `target_id: str` +- `stack_depth_before: int` +- `stack_depth_after: int` + +Validation must require: + +- `target_id` is a non-empty string; +- both depth fields are exact integers (`type(value) is int`), so booleans are rejected; +- both depths are nonnegative; +- `stack_depth_after == stack_depth_before + 1`. + +`to_payload()` must return exactly those three fields and no others. + +Export the new contract from `hsr_axis_sim/runtime_contracts/__init__.py`. + +### 4. Bind the legacy event to the already-existing runtime vocabulary + +Do not change `RuntimeEventType` enum values. + +Add a deterministic registry mapping: + +`extra_turn_queued -> RuntimeEventType.EXTRA_TURN_QUEUED` + +Normalized fields: + +- `action_id <- action_id` +- `actor_id <- actor_id` +- `target_id <- target_id` + +Preserve the existing sorted mapping-registry key order. + +Use a confirmed/bound semantic contract consistent with other production-observed mappings. Do not backfill the historical ARCH-002 mapping document. + +### 5. Preserve raw data and expose typed payload + +For a valid `extra_turn_queued` legacy event: + +- preserve exact raw input in `payload["legacy_data"]`; +- expose the validated typed payload in `payload["extra_turn_queue"]`; +- keep normal adapter provenance fields unchanged. + +Malformed structured queue observations must raise `LegacyEventSchemaError`; they must not degrade to `CONTENT_DEFINED` or silently drop invalid fields. + +### 6. Prove post-mutation trigger visibility + +Add a focused trigger test showing that when an `extra_turn_queued` trigger fires, it sees the target already appended and the stack depth equal to `stack_depth_after`. + +Do not change trigger dispatch semantics. + +### 7. Prove self-target behavior + +Add a dedicated self-target test: + +- actor and target IDs may be identical; +- exactly one target ID is appended; +- event provenance keeps `actor_id == target_id` when self-targeted; +- depth transitions are exact. + +### 8. Prove multi-target append order and LIFO resolution + +Add a dedicated multi-target test using explicit target order, for example `["first", "second"]`: + +- queue events are emitted in resolved/declaration order: first, then second; +- depth transitions are `0 -> 1`, then `1 -> 2`; +- stack after the action is `["first", "second"]`; +- subsequent `Timeline.next_turn()` calls resolve `second`, then `first`; +- global AV / normal timeline behavior remains unchanged by the extra-turn selection itself. + +This is a test of accepted simulator LIFO behavior only, not a claim about undocumented HSR priority semantics. + +### 9. Runtime capture proof + +Use the existing ARCH-012 capture pipeline to prove one target produces exactly three typed records: + +1. `ACTION_START` +2. `EXTRA_TURN_QUEUED` +3. `ACTION_END` + +The queued record must preserve: + +- action ID; +- actor ID; +- target ID; +- `payload["legacy_data"]`; +- exact `payload["extra_turn_queue"]`; +- existing adapter provenance; +- no unrelated numeric-values payload. + +### 10. Preserve all accepted regressions and reviewed fixtures + +All existing reviewed Golden fixture byte identities must remain unchanged. + +Required regression results after this task: + +- legacy regression: **20/20**; +- trace evidence: **2/2**; +- standalone runtime action-session Golden regression: **9/9**; +- production LIFO compatibility remains explicit and passing. + +ARCH-043 must not add a static GrantExtraTurn Golden and must not promote a new regression case. + +## Required tests + +Add a focused file: + +`hsr_axis_sim/tests/test_runtime_arch_043_extra_turn_observation.py` + +It must cover at minimum: + +1. frozen `RuntimeExtraTurnQueuedObservation` exact payload; +2. rejection of empty/non-string target IDs; +3. rejection of bool/float/string/negative queue-depth fields; +4. rejection unless `after == before + 1`; +5. adapter mapping to `EXTRA_TURN_QUEUED`; +6. exact normalized IDs; +7. exact raw `legacy_data` preservation; +8. exact typed `extra_turn_queue` payload; +9. malformed event rejection as `LegacyEventSchemaError`; +10. one-target production event data and event ordering; +11. trigger sees post-append stack; +12. self-target provenance and depth; +13. multi-target declared append/event order; +14. subsequent LIFO resolution in reverse append order; +15. runtime capture `ACTION_START -> EXTRA_TURN_QUEUED -> ACTION_END`; +16. `GrantExtraTurn` remains distinct from ImmediateAction/Advance/Delay/ChangeSpeed observations; +17. all accepted reviewed fixture identities remain exact; +18. legacy 20/20; +19. trace evidence 2/2; +20. runtime action-session 9/9; +21. existing production extra-turn LIFO behavior remains unchanged. + +Update stale historical source guards only where this newly authorized observation makes an old global-current assumption false. Historical milestones must continue protecting their original mechanic boundaries; do not weaken unrelated assertions. + +## Files / areas that must remain unchanged + +Unless a narrow historical test boundary must be updated because of this authorized observation, do not change: + +- `hsr_axis_sim/sim/timeline.py`; +- `hsr_axis_sim/sim/action.py`; +- `hsr_axis_sim/sim/unit.py`; +- `hsr_axis_sim/sim/state.py` except no change is expected; +- `hsr_axis_sim/runtime_contracts/enums.py`; +- runtime resource/action-axis observation semantics; +- runtime action-session regression manifest grammar/runner; +- `hsr_axis_sim/data/runtime_action_session_regression_manifest.json`; +- `hsr_axis_sim/data/regression_manifest.json`; +- every file under `hsr_axis_sim/data/runtime_golden_fixtures/`; +- Golden validator/comparator/first-divergence code; +- trace schema/version; +- ownership/SP/energy semantics; +- any other effect implementation. + +The only production simulator change should be observation emission around the existing append inside `GrantExtraTurn.apply()`. + +## Explicit exclusions + +Do not implement or infer: + +- real HSR extra-turn priority values; +- interrupt windows; +- ultimate priority; +- extra-action/follow-up semantics; +- FIFO/FILO alternatives; +- a generalized scheduler or priority queue; +- changes to accepted LIFO ordering; +- changes to Timeline selection/reset behavior; +- queue deduplication or replacement; +- automatic action selection; +- a static GrantExtraTurn Golden fixture; +- runtime regression promotion or manifest v1.9; +- generic effect DSL support; +- trace schema bump; +- video parsing/scraping; +- character database expansion; +- damage formulas; +- AI optimization; +- unrelated UI/refactor work. + +## Commands to run + +```bash +python -m compileall -q hsr_axis_sim +python -m pytest -q +python -m hsr_axis_sim.regression.runner --manifest hsr_axis_sim/data/regression_manifest.json --format text +python -m hsr_axis_sim.regression.runner --manifest hsr_axis_sim/data/regression_manifest.json --only trace_evidence --format text +python -m hsr_axis_sim.runtime_action_session_regression.runner --manifest hsr_axis_sim/data/runtime_action_session_regression_manifest.json --format text +``` + +Report real results only. + +## Acceptance criteria + +ARCH-043 is accepted only if all of the following are true: + +- existing `GrantExtraTurn` append semantics are unchanged; +- exactly one post-append `extra_turn_queued` event is emitted per resolved target; +- event fields are exactly the five specified legacy fields; +- strict frozen typed queue observation exists and rejects malformed depth transitions; +- existing `EXTRA_TURN_QUEUED` enum is used without enum changes; +- adapter preserves both raw and typed payloads; +- malformed structured queue events raise `LegacyEventSchemaError`; +- trigger sees post-mutation stack; +- self-target behavior is explicitly tested; +- multi-target append order is explicitly tested; +- subsequent resolution remains deterministic LIFO; +- one-target runtime capture is exactly three records in required order; +- no static Golden/regression promotion occurs; +- no Timeline or queue-selection semantics change occurs; +- all reviewed fixture bytes remain unchanged; +- full pytest passes; +- legacy 20/20 passes; +- trace evidence 2/2 passes; +- runtime action-session 9/9 passes; +- exclusions are respected; +- `LUMEN_RESULT.md` is updated with exact commands/results, warnings, unresolved issues, exclusions, and next milestone suggestion. + +## Final report format + +Update `hsr_axis_sim/LUMEN_RESULT.md` and include: + +1. task ID; +2. decision/status; +3. starting accepted main SHA; +4. implementation summary; +5. exact observation semantics and explicit non-claims about real HSR priority; +6. files added/modified; +7. tests added/updated; +8. exact commands executed; +9. exact real pass/fail counts and timings; +10. any intermediate CI failures and evidence-backed fixes; +11. warnings/errors; +12. unresolved issues; +13. confirmation that exclusions and locked areas were respected; +14. suggested next milestone only after acceptance. + +## Execution routing + +- ChatGPT model: **GPT-5.6 Sol** +- Codex reasoning: **High** + +Reason: the implementation is narrow, but it observes the core deterministic extra-turn queue whose LIFO semantics must not be accidentally changed. From 9f7edaab07920de83e1b0d34b8efb65d178b3cba Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:31:25 +0800 Subject: [PATCH 02/12] runtime: add strict extra-turn queue observation --- .../turn_order_observations.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 hsr_axis_sim/runtime_contracts/turn_order_observations.py diff --git a/hsr_axis_sim/runtime_contracts/turn_order_observations.py b/hsr_axis_sim/runtime_contracts/turn_order_observations.py new file mode 100644 index 0000000..b399e67 --- /dev/null +++ b/hsr_axis_sim/runtime_contracts/turn_order_observations.py @@ -0,0 +1,34 @@ +"""Schema-v1-compatible deterministic turn-order observation payloads.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class RuntimeExtraTurnQueuedObservation: + """One completed production append to the deterministic extra-turn stack.""" + + target_id: str + stack_depth_before: int + stack_depth_after: int + + def __post_init__(self) -> None: + if not isinstance(self.target_id, str) or not self.target_id.strip(): + raise ValueError("target_id must be a non-empty string") + for name in ("stack_depth_before", "stack_depth_after"): + value = getattr(self, name) + if type(value) is not int: + raise TypeError(f"{name} must be an integer") + if value < 0: + raise ValueError(f"{name} must be non-negative") + if self.stack_depth_after != self.stack_depth_before + 1: + raise ValueError("stack_depth_after must equal stack_depth_before + 1") + + def to_payload(self) -> dict[str, Any]: + return { + "target_id": self.target_id, + "stack_depth_before": self.stack_depth_before, + "stack_depth_after": self.stack_depth_after, + } From 1048a94a54aad4c262a1bfd316092010ac7813dc Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:31:33 +0800 Subject: [PATCH 03/12] runtime: export extra-turn queue observation --- hsr_axis_sim/runtime_contracts/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hsr_axis_sim/runtime_contracts/__init__.py b/hsr_axis_sim/runtime_contracts/__init__.py index 3f528b6..1d26073 100644 --- a/hsr_axis_sim/runtime_contracts/__init__.py +++ b/hsr_axis_sim/runtime_contracts/__init__.py @@ -38,5 +38,6 @@ from .resource_observations import RuntimeResourceChangeObservation from .serialization import canonical_json_bytes, canonical_json_dumps, to_canonical_data from .trace import RuntimeTraceRecord, TraceNumericValue +from .turn_order_observations import RuntimeExtraTurnQueuedObservation __all__ = [name for name in globals() if not name.startswith("_")] From 23df725538daf9b0815d299d29ad55c68b91d1e6 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:32:23 +0800 Subject: [PATCH 04/12] sim: observe GrantExtraTurn stack append --- hsr_axis_sim/sim/effects.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hsr_axis_sim/sim/effects.py b/hsr_axis_sim/sim/effects.py index 41eda75..acb5402 100644 --- a/hsr_axis_sim/sim/effects.py +++ b/hsr_axis_sim/sim/effects.py @@ -279,7 +279,22 @@ def apply(self, state: BattleState, action: object, turn_context: TurnContext) - class GrantExtraTurn(UnitEffect): def apply(self, state: BattleState, action: object, turn_context: TurnContext) -> None: for unit in self.target_units(state, action): + stack_depth_before = len(state.extra_turn_stack) state.extra_turn_stack.append(unit.id) + stack_depth_after = len(state.extra_turn_stack) + state.emit_event( + Event( + "extra_turn_queued", + { + "actor_id": getattr(action, "actor_id"), + "action_id": getattr(action, "id"), + "target_id": unit.id, + "stack_depth_before": stack_depth_before, + "stack_depth_after": stack_depth_after, + }, + ), + turn_context, + ) class DoesNotEndTurn(Effect): From d28435de96fcc2df089f40be94cb41e1aa929851 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:33:13 +0800 Subject: [PATCH 05/12] runtime: adapt extra-turn queue observations --- .../runtime_adapters/legacy_events.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/hsr_axis_sim/runtime_adapters/legacy_events.py b/hsr_axis_sim/runtime_adapters/legacy_events.py index 62e2687..eadba53 100644 --- a/hsr_axis_sim/runtime_adapters/legacy_events.py +++ b/hsr_axis_sim/runtime_adapters/legacy_events.py @@ -14,6 +14,7 @@ RuntimeActionDelayObservation, RuntimeEvent, RuntimeEventType, + RuntimeExtraTurnQueuedObservation, RuntimeImmediateActionObservation, RuntimeResourceChangeObservation, RuntimeResourceKind, @@ -159,6 +160,12 @@ def _bound_mapping( {"action_id": "action_id", "actor_id": "actor_id", "target_id": "unit_id"}, "Validate and expose ARCH-019 resource_change while preserving raw legacy data.", ), + _bound_mapping( + "extra_turn_queued", RuntimeEventType.EXTRA_TURN_QUEUED, + "hsr_axis_sim/sim/effects.py", + {"action_id": "action_id", "actor_id": "actor_id", "target_id": "target_id"}, + "Validate and expose ARCH-043 extra_turn_queue while preserving raw legacy data; infer no game priority.", + ), _bound_mapping( "skill_points_changed", RuntimeEventType.SKILL_POINTS_CHANGED, "hsr_axis_sim/sim/effects.py", @@ -332,6 +339,26 @@ def _immediate_action_payload( return observation.to_payload() +def _extra_turn_queue_payload( + legacy_event_type: str, + data: Mapping[str, object], +) -> dict[str, object] | None: + if legacy_event_type != "extra_turn_queued": + return None + + try: + observation = RuntimeExtraTurnQueuedObservation( + target_id=data["target_id"], + stack_depth_before=data["stack_depth_before"], + stack_depth_after=data["stack_depth_after"], + ) + except (KeyError, TypeError, ValueError) as exc: + raise LegacyEventSchemaError( + f"invalid {legacy_event_type} extra-turn queue observation: {exc}" + ) from exc + return observation.to_payload() + + def _speed_change_payload( legacy_event_type: str, data: Mapping[str, object], @@ -399,6 +426,7 @@ def adapt_legacy_event( action_advance = _action_advance_payload(legacy_event.type, legacy_event.data) action_delay = _action_delay_payload(legacy_event.type, legacy_event.data) immediate_action = _immediate_action_payload(legacy_event.type, legacy_event.data) + extra_turn_queue = _extra_turn_queue_payload(legacy_event.type, legacy_event.data) speed_change = _speed_change_payload(legacy_event.type, legacy_event.data) payload = { "adapter": { @@ -420,6 +448,8 @@ def adapt_legacy_event( payload["action_delay"] = action_delay if immediate_action is not None: payload["immediate_action"] = immediate_action + if extra_turn_queue is not None: + payload["extra_turn_queue"] = extra_turn_queue if speed_change is not None: payload["speed_change"] = speed_change @@ -454,4 +484,4 @@ def adapt_legacy_event_stream( return tuple( adapt_legacy_event(event, sequence=sequence, config=config) for sequence, event in enumerate(events, start=start_sequence) - ) + ) \ No newline at end of file From b8b983077b517a0f0e5ea8de3adb147c4c5e77a4 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:33:41 +0800 Subject: [PATCH 06/12] tests: lock ARCH-043 legacy extra-turn mapping --- .../tests/test_runtime_legacy_event_mapping.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/hsr_axis_sim/tests/test_runtime_legacy_event_mapping.py b/hsr_axis_sim/tests/test_runtime_legacy_event_mapping.py index ee121d6..f5c5214 100644 --- a/hsr_axis_sim/tests/test_runtime_legacy_event_mapping.py +++ b/hsr_axis_sim/tests/test_runtime_legacy_event_mapping.py @@ -50,18 +50,25 @@ {"action_id": "action_id", "actor_id": "actor_id", "target_id": "target_id"}, ), } +ARCH_043_EXPECTED = { + "extra_turn_queued": ( + RuntimeEventType.EXTRA_TURN_QUEUED, + {"action_id": "action_id", "actor_id": "actor_id", "target_id": "target_id"}, + ), +} CURRENT_EXPECTED = { **HISTORICAL_EXPECTED, **ARCH_031_EXPECTED, **ARCH_034_EXPECTED, **ARCH_037_EXPECTED, **ARCH_040_EXPECTED, + **ARCH_043_EXPECTED, } def test_exact_immutable_mapping_registry(): assert list(LEGACY_EVENT_MAPPINGS) == sorted(CURRENT_EXPECTED) - assert len(LEGACY_EVENT_MAPPINGS) == 13 + assert len(LEGACY_EVENT_MAPPINGS) == 14 for legacy_type, (runtime_type, fields) in CURRENT_EXPECTED.items(): mapping = LEGACY_EVENT_MAPPINGS[legacy_type] assert mapping.runtime_event_type is runtime_type @@ -74,9 +81,9 @@ def test_exact_immutable_mapping_registry(): LEGACY_EVENT_MAPPINGS["new"] = object() -def test_twelve_bound_contracts_and_one_unresolved_lifecycle(): +def test_thirteen_bound_contracts_and_one_unresolved_lifecycle(): bound = [mapping for mapping in LEGACY_EVENT_MAPPINGS.values() if mapping.legacy_event_type != "unit_defeated"] - assert len(bound) == 12 + assert len(bound) == 13 for mapping in bound: contract = mapping.semantic_contract assert contract.evidence_status is EvidenceStatus.CONFIRMED @@ -107,14 +114,16 @@ def test_arch_002_mapping_document_remains_exact_historical_projection(): assert item["source_refs"] == list(contract.source_refs) -def test_arch_031_034_037_and_040_mappings_are_additive_not_backfilled_into_arch_002_document(): +def test_later_runtime_mappings_are_additive_not_backfilled_into_arch_002_document(): assert "action_advanced" in LEGACY_EVENT_MAPPINGS assert "action_delayed" in LEGACY_EVENT_MAPPINGS assert "speed_changed" in LEGACY_EVENT_MAPPINGS assert "action_immediate" in LEGACY_EVENT_MAPPINGS + assert "extra_turn_queued" in LEGACY_EVENT_MAPPINGS document = json.loads((ROOT / "docs/runtime/LEGACY_EVENT_MAPPING_V1.json").read_text()) historical_types = {item["legacy_event_type"] for item in document} assert "action_advanced" not in historical_types assert "action_delayed" not in historical_types assert "speed_changed" not in historical_types assert "action_immediate" not in historical_types + assert "extra_turn_queued" not in historical_types From bd44f11ee3b34502aac525fd94748e867a83fba7 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:34:30 +0800 Subject: [PATCH 07/12] tests: cover ARCH-043 extra-turn observation --- ...runtime_arch_043_extra_turn_observation.py | 403 ++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 hsr_axis_sim/tests/test_runtime_arch_043_extra_turn_observation.py diff --git a/hsr_axis_sim/tests/test_runtime_arch_043_extra_turn_observation.py b/hsr_axis_sim/tests/test_runtime_arch_043_extra_turn_observation.py new file mode 100644 index 0000000..c5222a0 --- /dev/null +++ b/hsr_axis_sim/tests/test_runtime_arch_043_extra_turn_observation.py @@ -0,0 +1,403 @@ +from dataclasses import FrozenInstanceError +import hashlib +import inspect +from pathlib import Path + +import pytest + +from hsr_axis_sim.regression.manifest import load_regression_manifest +from hsr_axis_sim.regression.runner import run_regression +from hsr_axis_sim.runtime_action_captures import ( + SingleActionEventCaptureRequest, + execute_action_and_capture_pending_events, +) +from hsr_axis_sim.runtime_action_session_regression.manifest import ( + load_runtime_action_session_regression_manifest, +) +from hsr_axis_sim.runtime_action_session_regression.runner import ( + run_runtime_action_session_regression, +) +from hsr_axis_sim.runtime_adapters import ( + AmbiguousLegacyEventPolicy, + LegacyEventAdapterConfig, + LegacyEventSchemaError, + UnknownLegacyEventPolicy, + adapt_legacy_event, +) +from hsr_axis_sim.runtime_capture_cursors import PendingEventCaptureCursor +from hsr_axis_sim.runtime_contracts import ( + RuntimeEventType, + RuntimeExtraTurnQueuedObservation, +) +from hsr_axis_sim.runtime_exports import ( + EmptyTracePolicy, + TraceExportConfig, + TraceSequencePolicy, +) +from hsr_axis_sim.runtime_trace_bridges import LegacyEventTraceBridgeConfig +from hsr_axis_sim.sim.action import Action +from hsr_axis_sim.sim.effects import ( + AdvanceAction, + ChangeSpeed, + DelayAction, + Effect, + GrantExtraTurn, + ImmediateAction, +) +from hsr_axis_sim.sim.events import Event, Trigger +from hsr_axis_sim.sim.state import BattleState +from hsr_axis_sim.sim.timeline import Timeline +from hsr_axis_sim.sim.unit import Unit + + +ROOT = Path(__file__).parents[2] +LEGACY_MANIFEST_PATH = ROOT / "hsr_axis_sim" / "data" / "regression_manifest.json" +RUNTIME_MANIFEST_PATH = ( + ROOT / "hsr_axis_sim" / "data" / "runtime_action_session_regression_manifest.json" +) +FIXTURE_DIR = ROOT / "hsr_axis_sim" / "data" / "runtime_golden_fixtures" +FIXTURES = ( + ("arch_017_reviewed_action_session_expected.json", 3013, "f672ffaac9ef9296e4982a6fb61f4d0257b5c0506412bcf54eb1768334118c66"), + ("arch_021_reviewed_clamped_energy_expected.json", 2759, "4fe2c8b3c9c22821a49ff380c9635828e36f3c4a3bbbc13d2cc077dd4d97e605"), + ("arch_023_reviewed_clamped_skill_point_expected.json", 2744, "fc359b367c2922ed39e059f89ad16845ba215508292cfa43ca2ab6031c4c9ba9"), + ("arch_025_reviewed_energy_consume_expected.json", 2750, "7d61528687a5a2f499249e0f914f6f2f50975c7c153165eddd5e116f3ed19a75"), + ("arch_027_reviewed_skill_point_consume_expected.json", 2796, "d0dcf128f3a28f691324f4e9295b7bcd66460598186f6059d4619f55e8ae39ec"), + ("arch_032_reviewed_action_advance_expected.json", 2818, "ab73c224d06690b379d398a5bc2c4b38a1ed654dfd86866d564417432c29d3ce"), + ("arch_035_reviewed_action_delay_expected.json", 2728, "9efbb65defb5eacc12150d31d0530d9a94b43a42e2303ebca643911f98094c4d"), + ("arch_038_reviewed_change_speed_expected.json", 2604, "c23b34e0afffdfe4bee53d028e5ff21d946623300b169ba57e5ddfb69478df2a"), + ("arch_041_reviewed_immediate_action_expected.json", 2620, "7fd1594362b5bf9a95eec6f6472b2f17afa9dcfe10196d81ec6c970eab86eea1"), +) + + +def _adapter_config() -> LegacyEventAdapterConfig: + return LegacyEventAdapterConfig( + "extra-turn-observation-stream", + UnknownLegacyEventPolicy.REJECT, + AmbiguousLegacyEventPolicy.REJECT, + ) + + +def _capture_request() -> SingleActionEventCaptureRequest: + return SingleActionEventCaptureRequest( + PendingEventCaptureCursor(0, 0), + LegacyEventTraceBridgeConfig( + _adapter_config(), + 0, + TraceExportConfig( + "extra-turn-observation-trace", + TraceSequencePolicy.CONTIGUOUS, + EmptyTracePolicy.REJECT, + {"source": "arch-043"}, + ), + False, + ), + ) + + +def _queued_event_data(**overrides): + data = { + "actor_id": "actor", + "action_id": "grant-extra", + "target_id": "target", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + data.update(overrides) + return data + + +def test_extra_turn_queue_observation_is_frozen_strict_and_exact(): + observation = RuntimeExtraTurnQueuedObservation( + target_id="target", + stack_depth_before=0, + stack_depth_after=1, + ) + assert observation.to_payload() == { + "target_id": "target", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + with pytest.raises(FrozenInstanceError): + observation.stack_depth_after = 2 + + +@pytest.mark.parametrize( + "kwargs, error_type", + [ + ({"target_id": ""}, ValueError), + ({"target_id": 1}, ValueError), + ({"stack_depth_before": True}, TypeError), + ({"stack_depth_after": False}, TypeError), + ({"stack_depth_before": 0.0}, TypeError), + ({"stack_depth_after": 1.0}, TypeError), + ({"stack_depth_before": "0"}, TypeError), + ({"stack_depth_before": -1}, ValueError), + ({"stack_depth_after": -1}, ValueError), + ({"stack_depth_before": 1, "stack_depth_after": 1}, ValueError), + ({"stack_depth_before": 1, "stack_depth_after": 3}, ValueError), + ], +) +def test_extra_turn_queue_observation_rejects_malformed_contract(kwargs, error_type): + values = { + "target_id": "target", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + values.update(kwargs) + with pytest.raises(error_type): + RuntimeExtraTurnQueuedObservation(**values) + + +def test_legacy_extra_turn_queued_maps_to_dedicated_runtime_event_and_payload(): + data = _queued_event_data() + result = adapt_legacy_event( + Event("extra_turn_queued", data), sequence=5, config=_adapter_config() + ) + + assert result.event_type is RuntimeEventType.EXTRA_TURN_QUEUED + assert result.action_id == "grant-extra" + assert result.actor_id == "actor" + assert result.target_id == "target" + assert dict(result.payload["legacy_data"]) == data + assert dict(result.payload["extra_turn_queue"]) == { + "target_id": "target", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + assert result.payload["adapter"]["mapping_status"] == "BOUND" + assert result.payload["adapter"]["mechanic_id"] == "LEGACY_EVENT.EXTRA_TURN_QUEUED" + + +@pytest.mark.parametrize( + "mutation", + [ + lambda data: data.pop("stack_depth_after"), + lambda data: data.__setitem__("stack_depth_before", True), + lambda data: data.__setitem__("stack_depth_after", 2), + lambda data: data.__setitem__("target_id", ""), + ], +) +def test_malformed_extra_turn_queued_is_rejected_not_degraded(mutation): + data = _queued_event_data() + mutation(data) + with pytest.raises(LegacyEventSchemaError): + adapt_legacy_event( + Event("extra_turn_queued", data), sequence=0, config=_adapter_config() + ) + + +def test_production_grant_extra_turn_preserves_append_and_exact_event_order(): + actor = Unit("actor", "Actor", "ally", 100, current_av=20) + target = Unit("target", "Target", "ally", 100, current_av=80) + state = BattleState([actor, target]) + + Action( + "grant-extra", + "Grant Extra", + "actor", + effects=[GrantExtraTurn(target_ids=["target"])], + ends_turn=False, + ).execute(state) + + assert state.extra_turn_stack == ["target"] + assert target.current_av == 80 + assert [event.type for event in state.pending_events] == [ + "action_started", + "extra_turn_queued", + "action_finished", + ] + assert state.pending_events[1].data == { + "actor_id": "actor", + "action_id": "grant-extra", + "target_id": "target", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + + +class ObserveQueuedStack(Effect): + def apply(self, state, action, turn_context): + state.logs.append( + "observed-extra-stack:" + f"{list(state.extra_turn_stack)}:" + f"{action.event_data['stack_depth_after']}" + ) + + +def test_extra_turn_queued_trigger_observes_post_append_stack(): + actor = Unit("actor", "Actor", "ally", 100, current_av=20) + target = Unit("target", "Target", "ally", 100, current_av=80) + state = BattleState( + [actor, target], + triggers=[ + Trigger( + id="observe-extra-turn", + owner_id="actor", + event_type="extra_turn_queued", + condition={"type": "always"}, + effects=[ObserveQueuedStack()], + ) + ], + ) + + Action( + "grant-extra", + "Grant Extra", + "actor", + effects=[GrantExtraTurn(target_ids=["target"])], + ends_turn=False, + ).execute(state) + + assert state.extra_turn_stack == ["target"] + assert state.logs == [ + "trigger:observe-extra-turn", + "observed-extra-stack:['target']:1", + ] + + +def test_arch_012_capture_contains_exact_typed_three_record_extra_turn_trace(): + actor = Unit("actor", "Actor", "ally", 100, current_av=20) + target = Unit("target", "Target", "ally", 100, current_av=80) + state = BattleState([actor, target]) + action = Action( + "grant-extra", + "Grant Extra", + "actor", + effects=[GrantExtraTurn(target_ids=["target"])], + ends_turn=False, + ) + + result = execute_action_and_capture_pending_events( + state, action, request=_capture_request() + ) + + records = result.capture_result.capture_result.bridge_result.artifact.document.records + assert [record.sequence for record in records] == [0, 1, 2] + assert [record.event.event_type for record in records] == [ + RuntimeEventType.ACTION_START, + RuntimeEventType.EXTRA_TURN_QUEUED, + RuntimeEventType.ACTION_END, + ] + queued = records[1].event + assert queued.action_id == "grant-extra" + assert queued.actor_id == "actor" + assert queued.target_id == "target" + assert dict(queued.payload["extra_turn_queue"]) == { + "target_id": "target", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + assert dict(queued.payload["legacy_data"]) == _queued_event_data() + assert queued.payload["adapter"]["mechanic_id"] == "LEGACY_EVENT.EXTRA_TURN_QUEUED" + assert records[1].numeric_values == {} + assert result.next_cursor == PendingEventCaptureCursor(3, 3) + + +def test_self_target_extra_turn_preserves_actor_target_identity_and_depth(): + actor = Unit("actor", "Actor", "ally", 100, current_av=80) + state = BattleState([actor]) + + Action( + "self-extra", + "Self Extra", + "actor", + effects=[GrantExtraTurn(target_ids=["actor"])], + ends_turn=False, + ).execute(state) + + assert state.extra_turn_stack == ["actor"] + event = state.pending_events[1] + assert event.type == "extra_turn_queued" + assert event.data == { + "actor_id": "actor", + "action_id": "self-extra", + "target_id": "actor", + "stack_depth_before": 0, + "stack_depth_after": 1, + } + + +def test_multi_target_queue_events_preserve_append_order_then_resolve_lifo(): + actor = Unit("actor", "Actor", "ally", 100, current_av=90) + first = Unit("first", "First", "ally", 100, current_av=80) + second = Unit("second", "Second", "ally", 100, current_av=70) + state = BattleState([actor, first, second]) + + Action( + "multi-extra", + "Multi Extra", + "actor", + effects=[GrantExtraTurn(target_ids=["first", "second"])], + ends_turn=False, + ).execute(state) + + queued = [event for event in state.pending_events if event.type == "extra_turn_queued"] + assert [event.data["target_id"] for event in queued] == ["first", "second"] + assert [ + (event.data["stack_depth_before"], event.data["stack_depth_after"]) + for event in queued + ] == [(0, 1), (1, 2)] + assert state.extra_turn_stack == ["first", "second"] + + before_av = {unit.id: unit.current_av for unit in state.units} + first_turn = Timeline.next_turn(state) + second_turn = Timeline.next_turn(state) + assert [first_turn.actor_id, second_turn.actor_id] == ["second", "first"] + assert first_turn.is_extra_turn is True + assert second_turn.is_extra_turn is True + assert state.global_av == 0 + assert {unit.id: unit.current_av for unit in state.units} == before_av + + +def test_grant_extra_turn_observation_remains_distinct_from_axis_mutations(): + source = inspect.getsource(GrantExtraTurn) + assert "extra_turn_queued" in source + assert "extra_turn_stack.append" in source + assert "action_advanced" not in source + assert "action_delayed" not in source + assert "speed_changed" not in source + assert "action_immediate" not in source + assert "current_av" not in source + assert "priority" not in source.lower() + + assert "extra_turn_queued" not in inspect.getsource(AdvanceAction) + assert "extra_turn_queued" not in inspect.getsource(DelayAction) + assert "extra_turn_queued" not in inspect.getsource(ChangeSpeed) + assert "extra_turn_queued" not in inspect.getsource(ImmediateAction) + + +def test_all_nine_reviewed_fixture_byte_identities_remain_exact(): + for filename, size, digest in FIXTURES: + payload = (FIXTURE_DIR / filename).read_bytes() + assert len(payload) == size + assert hashlib.sha256(payload).hexdigest() == digest + + +def test_existing_regression_lanes_remain_accepted(): + legacy = load_regression_manifest(LEGACY_MANIFEST_PATH) + complete = run_regression(manifest=legacy) + trace = run_regression(manifest=legacy, only="trace_evidence") + runtime = run_runtime_action_session_regression( + load_runtime_action_session_regression_manifest(RUNTIME_MANIFEST_PATH) + ) + + assert complete.passed is True + assert complete.total == 20 + assert complete.passed_count == 20 + assert trace.passed is True + assert trace.total == 2 + assert trace.passed_count == 2 + assert runtime.passed is True + assert runtime.total == 9 + assert runtime.passed_count == 9 + + +def test_production_extra_turn_lifo_compatibility_is_unchanged(): + units = [Unit(name, name, "ally", 100) for name in ("first", "second", "third")] + state = BattleState(units=units, extra_turn_stack=["first", "second", "third"]) + + assert [Timeline.next_turn(state).actor_id for _ in range(3)] == [ + "third", + "second", + "first", + ] From cb887f6a47a7a9b1b01eeb8472287a75ea53d885 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:36:28 +0800 Subject: [PATCH 08/12] tests: preserve ARCH-031 boundary after extra-turn observation --- .../test_runtime_arch_031_advance_action_observation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hsr_axis_sim/tests/test_runtime_arch_031_advance_action_observation.py b/hsr_axis_sim/tests/test_runtime_arch_031_advance_action_observation.py index def003f..0392d34 100644 --- a/hsr_axis_sim/tests/test_runtime_arch_031_advance_action_observation.py +++ b/hsr_axis_sim/tests/test_runtime_arch_031_advance_action_observation.py @@ -382,7 +382,8 @@ def test_arch_031_scope_preserves_later_authorized_axis_effects(): assert "action_delayed" not in extra_turn_source assert "speed_changed" not in extra_turn_source assert "action_immediate" not in extra_turn_source - assert "emit_event" not in extra_turn_source + assert "extra_turn_queued" in extra_turn_source + assert "emit_event" in extra_turn_source def test_production_extra_turn_lifo_compatibility_is_unchanged(): @@ -390,4 +391,4 @@ def test_production_extra_turn_lifo_compatibility_is_unchanged(): state.extra_turn_stack.extend(["first", "second", "third"]) assert state.extra_turn_stack.pop() == "third" assert state.extra_turn_stack.pop() == "second" - assert state.extra_turn_stack.pop() == "first" + assert state.extra_turn_stack.pop() == "first" \ No newline at end of file From 67f8363e2310f8e3006d1bf9b5b357ce74082d8c Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:39:02 +0800 Subject: [PATCH 09/12] tests: preserve ARCH-034 boundary after extra-turn observation --- .../tests/test_runtime_arch_034_delay_action_observation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hsr_axis_sim/tests/test_runtime_arch_034_delay_action_observation.py b/hsr_axis_sim/tests/test_runtime_arch_034_delay_action_observation.py index ba63416..ef36e19 100644 --- a/hsr_axis_sim/tests/test_runtime_arch_034_delay_action_observation.py +++ b/hsr_axis_sim/tests/test_runtime_arch_034_delay_action_observation.py @@ -466,7 +466,8 @@ def test_arch_034_scope_preserves_later_authorized_axis_observations(): assert "action_delayed" not in extra_turn_source assert "speed_changed" not in extra_turn_source assert "action_immediate" not in extra_turn_source - assert "emit_event" not in extra_turn_source + assert "extra_turn_queued" in extra_turn_source + assert "emit_event" in extra_turn_source def test_production_extra_turn_lifo_compatibility_is_unchanged(): From 432fab3c3e58c59695b3233e4f377519b5c6a2d0 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:39:52 +0800 Subject: [PATCH 10/12] tests: preserve ARCH-037 boundary after extra-turn observation --- .../tests/test_runtime_arch_037_change_speed_observation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hsr_axis_sim/tests/test_runtime_arch_037_change_speed_observation.py b/hsr_axis_sim/tests/test_runtime_arch_037_change_speed_observation.py index 412e89b..72befc2 100644 --- a/hsr_axis_sim/tests/test_runtime_arch_037_change_speed_observation.py +++ b/hsr_axis_sim/tests/test_runtime_arch_037_change_speed_observation.py @@ -370,7 +370,8 @@ def test_arch_037_scope_preserves_other_axis_effect_observations_and_exclusions( extra_turn_source = inspect.getsource(GrantExtraTurn) assert "speed_changed" not in extra_turn_source assert "action_immediate" not in extra_turn_source - assert "emit_event" not in extra_turn_source + assert "extra_turn_queued" in extra_turn_source + assert "emit_event" in extra_turn_source def test_all_seven_reviewed_fixture_byte_identities_remain_exact(): From 4851c6e551313082dc38c2b3ae8a763357e8624d Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:40:43 +0800 Subject: [PATCH 11/12] tests: preserve ARCH-040 boundary after extra-turn observation --- .../test_runtime_arch_040_immediate_action_observation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hsr_axis_sim/tests/test_runtime_arch_040_immediate_action_observation.py b/hsr_axis_sim/tests/test_runtime_arch_040_immediate_action_observation.py index 88834fc..cfa7afc 100644 --- a/hsr_axis_sim/tests/test_runtime_arch_040_immediate_action_observation.py +++ b/hsr_axis_sim/tests/test_runtime_arch_040_immediate_action_observation.py @@ -361,10 +361,11 @@ def test_advance_delay_and_change_speed_observations_remain_distinct(): assert "speed_changed" not in immediate_source -def test_grant_extra_turn_remains_separate_unobserved_and_lifo_unchanged(): +def test_grant_extra_turn_remains_separate_observed_and_lifo_unchanged(): source = inspect.getsource(GrantExtraTurn) assert "action_immediate" not in source - assert "emit_event" not in source + assert "extra_turn_queued" in source + assert "emit_event" in source state = BattleState([]) state.extra_turn_stack.extend(["first", "second", "third"]) @@ -407,4 +408,4 @@ def test_production_extra_turn_lifo_compatibility_is_unchanged(): "third", "second", "first", - ] + ] \ No newline at end of file From 6bf42798dcea90c5d4e2be28afa0295c03784a31 Mon Sep 17 00:00:00 2001 From: RayzerCat76 Date: Thu, 27 Aug 2026 08:42:00 +0800 Subject: [PATCH 12/12] docs: report ARCH-043 completion --- hsr_axis_sim/LUMEN_RESULT.md | 290 ++++++++++++++++++++--------------- 1 file changed, 163 insertions(+), 127 deletions(-) diff --git a/hsr_axis_sim/LUMEN_RESULT.md b/hsr_axis_sim/LUMEN_RESULT.md index 2187f39..d010937 100644 --- a/hsr_axis_sim/LUMEN_RESULT.md +++ b/hsr_axis_sim/LUMEN_RESULT.md @@ -1,4 +1,4 @@ -# HSR-RUNTIME-ARCH-042 — ImmediateAction Static Golden Regression Promotion +# HSR-RUNTIME-ARCH-043 — GrantExtraTurn Runtime Observation Contract ## Status @@ -6,110 +6,142 @@ PASS — proceed ## Task ID -`HSR-RUNTIME-ARCH-042` +`HSR-RUNTIME-ARCH-043` ## Current confirmed base -- Accepted `main` before this task: `e8492f8dd35a11b0f47fd1315e871cc5500b335c`. -- Baseline validation before this task: - - pytest: **1635 passed in 10.25s**; +- Accepted `main` before this task: `23be13799facfe63152d727459f345f03e87fac6`. +- Baseline post-merge validation before this task, GitHub Actions run #257: + - pytest: **1687 passed in 10.16s**; - legacy regression: **20/20**; - trace evidence: **2/2**; - - standalone runtime action-session Golden regression: **8/8**. + - standalone runtime action-session Golden regression: **9/9**. ## Objective completed -Promoted the already accepted ARCH-041 reviewed static ImmediateAction Golden into the dedicated standalone runtime action-session regression lane as the ninth locked reviewed case, without changing production ImmediateAction semantics or the accepted Golden fixture bytes. - -## Implementation summary - -- Evolved the standalone `runtime_action_session_regression` manifest grammar from v1.7 to v1.8. -- Added explicit historical constant `RUNTIME_ACTION_SESSION_REGRESSION_VERSION_1_7 = "1.7"` and retained the exact supported version chain from v1.0 through v1.8. -- Preserved historical grammar boundaries: - - `ACTION_ADVANCE` remains v1.5+; - - `ACTION_DELAY` remains v1.6+; - - `CHANGE_SPEED` remains v1.7+; - - `IMMEDIATE_ACTION` is accepted only in v1.8. -- Added frozen `RuntimeActionSessionRegressionImmediateActionSetup` with exact fields: - - `target_id`; - - `target_name`; - - `team`; - - `base_speed`; - - `initial_av`; - - `action_index`. -- ImmediateAction setup validation remains closed and strict: - - exact fields only; - - non-empty target/name/team strings; - - finite non-boolean `base_speed` and `initial_av`; - - positive `base_speed`; - - exact nonnegative in-range integer `action_index`; - - no new lower bound on `initial_av`. -- Extended the runner only with explicit production construction: - - `ImmediateAction(target_ids=[setup.target_id])` at the declared action index. -- Added no generic effect/action-axis DSL, dynamic import, class lookup, `eval`, `exec`, or generic kwargs path. -- Updated the standalone runtime manifest to v1.8 and appended the accepted ARCH-041 case as case 9 after the existing eight. -- Preserved the first eight case IDs/order and all reviewed Golden fixture byte identities. -- Updated stale historical/current tests only where later authorized promotion made their old global-current assumptions obsolete; historical milestone prefixes remain independently asserted. - -## Ninth locked case - -- Case ID: `arch-041-reviewed-static-immediate-action` -- Expected file: `hsr_axis_sim/data/runtime_golden_fixtures/arch_041_reviewed_immediate_action_expected.json` -- Expected size: **2620 bytes** -- Expected SHA-256: **`7fd1594362b5bf9a95eec6f6472b2f17afa9dcfe10196d81ec6c970eab86eea1`** -- Stream ID: `arch-041-reviewed-axis` -- Actor/target ID: `immediate-actor` -- Action: `reviewed-immediate-action`, `ends_turn=False` -- Setup: `IMMEDIATE_ACTION`, base speed `100`, initial AV `80`, action index `0` -- Runtime record count: **3** -- Accepted actual runtime trace SHA-256 in green PR CI run #255: **`b41181b9bb09ec516d27f78a99ef455a69c2b5e678d93f8eaa5f94effdde8cb7`**. - -The generated actual trace digest is provenance for the runtime-produced actual trace. It is not claimed to equal the pinned expected-file digest; the existing Golden comparison validates the expected and actual record streams under the existing validation semantics. +Made the already-existing production `GrantExtraTurn` extra-turn-stack append observable through the runtime trace pipeline without changing target resolution, append order, LIFO resolution, Timeline selection, action-value behavior, or the nine accepted reviewed Golden fixtures. + +This task observes the simulator's deterministic queue mutation only. It does **not** claim or infer real Honkai: Star Rail priority values, interrupt windows, ultimate priority, extra-action/follow-up behavior, or any undocumented scheduling rule. + +## Accepted simulator semantics preserved + +For each target resolved by the existing `UnitEffect.target_units()` path, `GrantExtraTurn.apply()` now records the queue depth around the existing append while preserving the append itself: + +```python +stack_depth_before = len(state.extra_turn_stack) +state.extra_turn_stack.append(unit.id) +stack_depth_after = len(state.extra_turn_stack) +``` + +It then emits one post-mutation legacy event: + +`extra_turn_queued` + +with exactly: + +- `actor_id`; +- `action_id`; +- `target_id`; +- `stack_depth_before`; +- `stack_depth_after`. + +The event is emitted after the append, so triggers observe the already-mutated stack. Target order is unchanged. `Timeline.next_turn()` remains unchanged and still resolves the accepted simulator stack by `pop()` from the end, so targets appended `[first, second]` resolve as `second`, then `first`. + +Extra-turn selection still does not advance global AV or mutate the normal timeline AV snapshot merely by selecting the queued turn. + +## Runtime contract added + +Added frozen `RuntimeExtraTurnQueuedObservation` in: + +`hsr_axis_sim/runtime_contracts/turn_order_observations.py` + +Exact fields: + +- `target_id: str`; +- `stack_depth_before: int`; +- `stack_depth_after: int`. + +Validation requires: + +- non-empty string target ID; +- exact integers for both depth fields, rejecting booleans and floats; +- nonnegative depths; +- `stack_depth_after == stack_depth_before + 1`. + +`to_payload()` returns exactly those three fields. The contract is exported from `hsr_axis_sim/runtime_contracts/__init__.py`. + +## Runtime adapter binding + +The existing runtime vocabulary value `RuntimeEventType.EXTRA_TURN_QUEUED` is reused; `runtime_contracts/enums.py` is unchanged. + +Added deterministic legacy mapping: + +`extra_turn_queued -> EXTRA_TURN_QUEUED` + +Normalized IDs: + +- `action_id <- action_id`; +- `actor_id <- actor_id`; +- `target_id <- target_id`. + +For valid events the adapter preserves: + +- exact raw input in `payload["legacy_data"]`; +- validated structured queue observation in `payload["extra_turn_queue"]`; +- existing adapter provenance. + +Malformed structured queue events raise `LegacyEventSchemaError`; they do not degrade to `CONTENT_DEFINED` and are not silently repaired. + +The current mapping registry is 14 entries with 13 bound mappings and the pre-existing unresolved `unit_defeated` lifecycle mapping. The historical ARCH-002 mapping document remains its exact nine-entry historical projection and does not backfill `extra_turn_queued`. ## Files added -- `LUMEN_TASK_HSR_RUNTIME_ARCH_042.md` -- `hsr_axis_sim/tests/test_runtime_arch_042_immediate_action_regression_promotion.py` +- `LUMEN_TASK_HSR_RUNTIME_ARCH_043.md` +- `hsr_axis_sim/runtime_contracts/turn_order_observations.py` +- `hsr_axis_sim/tests/test_runtime_arch_043_extra_turn_observation.py` ## Files modified - `hsr_axis_sim/LUMEN_RESULT.md` -- `hsr_axis_sim/data/runtime_action_session_regression_manifest.json` -- `hsr_axis_sim/runtime_action_session_regression/manifest.py` -- `hsr_axis_sim/runtime_action_session_regression/runner.py` -- `hsr_axis_sim/tests/test_runtime_arch_018_standalone_regression.py` -- `hsr_axis_sim/tests/test_runtime_arch_033_action_advance_regression_promotion.py` -- `hsr_axis_sim/tests/test_runtime_arch_035_static_action_delay_golden_fixture.py` -- `hsr_axis_sim/tests/test_runtime_arch_036_action_delay_regression_promotion.py` -- `hsr_axis_sim/tests/test_runtime_arch_038_static_change_speed_golden_fixture.py` -- `hsr_axis_sim/tests/test_runtime_arch_039_change_speed_regression_promotion.py` +- `hsr_axis_sim/runtime_contracts/__init__.py` +- `hsr_axis_sim/runtime_adapters/legacy_events.py` +- `hsr_axis_sim/sim/effects.py` +- `hsr_axis_sim/tests/test_runtime_legacy_event_mapping.py` +- `hsr_axis_sim/tests/test_runtime_arch_031_advance_action_observation.py` +- `hsr_axis_sim/tests/test_runtime_arch_034_delay_action_observation.py` +- `hsr_axis_sim/tests/test_runtime_arch_037_change_speed_observation.py` - `hsr_axis_sim/tests/test_runtime_arch_040_immediate_action_observation.py` -- `hsr_axis_sim/tests/test_runtime_arch_041_static_immediate_action_golden_fixture.py` - -## Tests added / updated - -Focused ARCH-042 coverage verifies: - -- supported manifest versions are exactly v1.0 through v1.8; -- v1.7 remains explicit historical grammar and rejects `IMMEDIATE_ACTION`; -- v1.8 accepts the exact frozen ImmediateAction setup; -- missing/unknown fields are rejected; -- identity strings are strict non-empty strings; -- numeric fields are finite and reject booleans; -- `base_speed` must be positive; -- `action_index` must be an exact nonnegative in-range integer; -- zero and negative finite initial AV remain representable; -- `CHANGE_SPEED` remains valid in v1.7/v1.8 and rejected before v1.7; -- current manifest contains exactly nine cases in the accepted order; -- ninth case has the exact accepted ARCH-041 path, digest, identity and setup; -- runtime lane passes exactly `9/9` with record counts `[4, 3, 3, 3, 3, 3, 3, 3, 3]`; -- controlled harness-only `initial_av=60` mutation produces first divergence at record index `1`, path `/event/payload/immediate_action/before_av`, expected `80`, actual `60`; -- reviewed Golden fixture byte identities remain exact; -- regression harness remains explicit and closed, with no `GrantExtraTurn` support or generic loader; -- ARCH-033, ARCH-036 and ARCH-039 historical prefixes remain independently protected; -- legacy regression remains `20/20`; -- trace evidence remains `2/2`; -- production extra-turn LIFO remains unchanged. + +## Focused tests added / updated + +ARCH-043 coverage proves: + +1. `RuntimeExtraTurnQueuedObservation` is frozen and serializes the exact three-field payload; +2. empty/non-string target IDs are rejected; +3. bool/float/string/negative queue-depth values are rejected; +4. depth transitions other than exact `before + 1` are rejected; +5. `extra_turn_queued` maps to `RuntimeEventType.EXTRA_TURN_QUEUED`; +6. normalized action/actor/target IDs are exact; +7. raw `legacy_data` is preserved exactly; +8. typed `extra_turn_queue` payload is exact; +9. malformed structured legacy events raise `LegacyEventSchemaError`; +10. one-target production event order is exactly `action_started -> extra_turn_queued -> action_finished`; +11. the event data contains exactly the five specified legacy fields; +12. an `extra_turn_queued` trigger sees the already-appended target and post-append depth; +13. self-target actor/target identity and depth are exact; +14. multi-target events preserve declared append order; +15. multi-target depth transitions are `0 -> 1` then `1 -> 2`; +16. subsequent `Timeline.next_turn()` calls resolve the accepted stack LIFO in reverse append order; +17. extra-turn selection leaves global AV and normal unit AV snapshots unchanged; +18. ARCH-012 capture produces exactly `ACTION_START -> EXTRA_TURN_QUEUED -> ACTION_END`; +19. `GrantExtraTurn` remains distinct from Advance/Delay/ChangeSpeed/ImmediateAction observation families; +20. all nine reviewed Golden fixture byte identities remain exact; +21. legacy regression remains `20/20`; +22. trace evidence remains `2/2`; +23. standalone runtime action-session Golden regression remains `9/9`; +24. pre-existing production LIFO compatibility remains explicit and passing. + +Historical ARCH-031/034/037/040 source guards were updated narrowly to permit the later-authorized `extra_turn_queued` observation while continuing to assert that `GrantExtraTurn` does not emit their action-axis event types. ## Exact commands executed by CI @@ -125,15 +157,15 @@ python -m hsr_axis_sim.runtime_action_session_regression.runner --manifest hsr_a Authoritative green PR validation before this completion-report update: -GitHub Actions `HSR Axis Sim Validation`, PR #47, run **#255** (`33026657321`), job **`98369501065`**, branch head `0e378dca3f89e9c5d3772f9366e77166b4b66124`: +GitHub Actions `HSR Axis Sim Validation`, PR #48, run **#262** (`33027644428`), job **`98372622604`**, branch head `4851c6e551313082dc38c2b3ae8a763357e8624d`: - compile: **PASS**; -- pytest: **1687 passed in 9.81s**; +- pytest: **1713 passed in 9.85s**; - legacy locked regression: **20/20**; - trace evidence: **2/2**; - standalone runtime action-session Golden regression: **9/9**. -Runtime record counts were exactly: +The nine runtime action-session reviewed cases and their accepted record counts remain: 1. `arch-017-reviewed-static-action-session` — 4; 2. `arch-021-reviewed-static-clamped-energy` — 3; @@ -145,67 +177,71 @@ Runtime record counts were exactly: 8. `arch-038-reviewed-static-change-speed` — 3; 9. `arch-041-reviewed-static-immediate-action` — 3. -## Validation history / resolved failures +No tenth runtime regression case was added. -A verified intermediate PR CI run **#248** (`32920547726`), job **`98033145142`**, compiled successfully and produced **7 failed, 1680 passed in 8.58s**. All seven failures were stale historical/current test boundaries after the authorized ninth-case promotion: - -1. ARCH-033 source guard still forbade later-authorized `ImmediateAction`; -2. ARCH-035 still expected the current runtime lane to total 8; -3. ARCH-036 source guard still forbade later-authorized `ImmediateAction`; -4. ARCH-038 still expected the current runtime lane to total 8; -5. ARCH-040 still expected the current runtime lane to total 8; -6. ARCH-041 still required its fixture to be absent from the runtime manifest; -7. ARCH-041 still expected the current runtime lane to total 8. +## Validation history / resolved failures -Those tests were updated narrowly so each historical milestone continues to protect its original prefix/closed grammar while allowing the explicitly authorized ARCH-042 promotion. No core ARCH-042 setup/parser/runner/manifest/Golden behavior failed in that run. Run #255 then passed the complete workflow. +Initial PR validation run **#258** (`33027318210`), job **`98371603615`**, branch head `bd44f11ee3b34502aac525fd94748e867a83fba7`: -Earlier development CI also exposed stale ARCH-018/current-version and ARCH-039 historical-boundary assumptions. Those were corrected before the fully verified run #248 history above; this report does not assign unverified exact run/count metadata to those earlier iterations. +- compile: **PASS**; +- pytest: **4 failed, 1709 passed in 10.76s**; +- downstream regression lanes were skipped because the pytest gate failed. -## Controlled first-divergence proof +All four failures were stale historical source guards that still required `GrantExtraTurn` to emit no event at all: -Changing only case 9 setup initial AV from `80` to `60` leaves production ImmediateAction final AV at `0`, while the existing Golden comparison returns: +1. ARCH-031 action-advance observation scope guard; +2. ARCH-034 action-delay observation scope guard; +3. ARCH-037 speed-change observation scope guard; +4. ARCH-040 ImmediateAction observation scope guard. -- record index: **1**; -- path: **`/event/payload/immediate_action/before_av`**; -- expected: **80**; -- actual: **60**. +No new ARCH-043 contract, adapter, production queue mutation, trigger, capture, self-target, multi-target, LIFO, or regression-identity test failed in that run. -No comparator or first-divergence semantics were changed. +The four historical guards were then updated narrowly so they continue to reject cross-mechanic action-axis events but explicitly allow the newly authorized `extra_turn_queued` observation. Run #262 then passed the complete workflow. ## Locked areas confirmed unchanged -- `hsr_axis_sim/sim/**` unchanged. -- `hsr_axis_sim/runtime_contracts/**` unchanged. -- `hsr_axis_sim/runtime_adapters/**` unchanged. +- `hsr_axis_sim/runtime_contracts/enums.py` unchanged; the pre-existing `EXTRA_TURN_QUEUED` value is reused. +- `hsr_axis_sim/sim/timeline.py` unchanged. +- `hsr_axis_sim/sim/action.py` unchanged. +- `hsr_axis_sim/sim/state.py` unchanged. +- `hsr_axis_sim/sim/unit.py` unchanged. +- runtime resource and action-axis observation semantics unchanged. +- runtime action-session regression manifest, grammar, and runner unchanged. +- `hsr_axis_sim/data/runtime_action_session_regression_manifest.json` unchanged at v1.8 / 9 cases. - `hsr_axis_sim/data/regression_manifest.json` unchanged. -- all files under `hsr_axis_sim/data/runtime_golden_fixtures/**` unchanged, including ARCH-041. -- Golden validator/comparator/divergence implementation unchanged. +- every reviewed Golden fixture under `hsr_axis_sim/data/runtime_golden_fixtures/` unchanged. +- Golden validator/comparator/first-divergence implementation unchanged. - trace schema/version unchanged. -- production Advance/Delay/ChangeSpeed/ImmediateAction behavior and events unchanged. -- `GrantExtraTurn` semantics unchanged and still unsupported by this regression grammar. -- Timeline semantics unchanged. -- production extra-turn LIFO unchanged. +- ownership/SP/energy semantics unchanged. +- Advance/Delay/ChangeSpeed/ImmediateAction production semantics unchanged. + +The only production simulator modification is observation emission around the pre-existing append inside `GrantExtraTurn.apply()`. ## Warnings / errors -- No compile, pytest, legacy-regression, trace-evidence, or runtime-action-session-regression failure remains in run #255. +- No compile, pytest, legacy-regression, trace-evidence, or runtime-action-session-regression failure remains in run #262. - Existing nonblocking GitHub Actions warning remains: `actions/checkout@v4` and `actions/setup-python@v5` target Node 20 and are forced onto Node 24. - Existing upstream Node `punycode` and `url.parse()` deprecation notices remain unrelated to simulator correctness. ## Unresolved issues -None blocking HSR-RUNTIME-ARCH-042 acceptance. +None blocking HSR-RUNTIME-ARCH-043 acceptance. -`GrantExtraTurn` remains intentionally outside this runtime regression grammar. Actual HSR scheduling semantics remain separate from the simulator's accepted deterministic LIFO implementation and must not be inferred without a dedicated evidence-backed task. +Real HSR extra-turn scheduling/priority semantics remain intentionally unresolved. This task only exposes the already accepted simulator LIFO stack mutation and must not be used as evidence that the game implements the same hidden scheduler. ## Exclusions confirmation -Respected: no Golden fixture generation or modification, no legacy manifest promotion, no simulator changes, no runtime observation contract/adapter changes, no generic effect/action-axis DSL, no ImmediateAction formula/observation changes, no GrantExtraTurn support, no automatic action selection, no priority/action-family/interrupt/extra-turn inference, no comparator/divergence/Golden changes, no trace schema bump, no video parsing/scraping, no character database expansion, no AI optimization, and no unrelated UI/refactor work. +Respected: no real-HSR priority inference, no interrupt windows, no ultimate-priority rules, no extra-action/follow-up semantics, no FIFO alternative, no generalized scheduler/priority queue, no Timeline change, no queue deduplication/replacement, no automatic action selection, no static GrantExtraTurn Golden, no runtime regression promotion, no manifest v1.9, no generic effect DSL, no enum change, no trace schema bump, no comparator/divergence change, no video parsing/scraping, no character database expansion, no damage formula work, no AI optimization, and no unrelated UI/refactor work. ## Suggested next milestone -Do not begin a new milestone until ARCH-042 is accepted on canonical `main` with post-merge CI. +Do not begin a new milestone until ARCH-043 is accepted on canonical `main` with post-merge CI. + +After acceptance, the smallest safe continuation is expected to be **HSR-RUNTIME-ARCH-044 — Reviewed Static GrantExtraTurn Golden Fixture**: manually construct and review one deterministic static Golden for the accepted queue observation and existing LIFO simulator behavior, prove production match and controlled first divergence, and do **not** promote it into the runtime regression lane in the same task. + +Recommended routing for ARCH-044 after ARCH-043 acceptance: -After acceptance, inspect the repository frontier before assigning the next task. A likely candidate is a dedicated `GrantExtraTurn` runtime observation contract, but it must be scoped separately from actual HSR scheduling semantics and must preserve the accepted production LIFO behavior unless an explicit evidence-backed task authorizes otherwise. +- ChatGPT: **GPT-5.6 Terra**; +- Codex reasoning: **High**. -Recommended routing if that becomes the next task: ChatGPT **GPT-5.6 Sol**; Codex reasoning **High**, because extra-turn queue semantics are core deterministic turn-order behavior. +The production semantics are already locked; ARCH-044 is deterministic fixture construction and validation rather than a new scheduler-semantic change.