diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 759f40d..fb8cc0a 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -27,8 +27,8 @@ The map audit currently sees: every registered action and position family except documented special cases covered by dedicated smoke fixtures. - Pair seed simulation, so Insta/OF soft/hard metadata and formatter outputs - prove locked determinism, content-only reroll behavior, and pose-only reroll - behavior. + prove locked determinism and person/scene/content/pose/expression/ + composition reroll behavior. ## Architectural Finding @@ -647,8 +647,8 @@ Near-term: - Keep same-room pair continuity synchronized in both assembled prompt text and `hardcore_row.scene_text`; `tools/prompt_smoke.py` covers this drift case. - Keep pair seed behavior synchronized across soft/hard rows; the route - simulator now checks locked pair determinism, content-only soft outfit - rerolls, and pose-only hard-action rerolls. + simulator now checks locked pair determinism and person, scene, content, + pose, expression, and composition rerolls. Medium-term: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 393dd8b..bd89a3a 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -1006,6 +1006,8 @@ issues for: - route-family coverage for registered action and position families, excluding only documented special cases that have their own smoke fixtures; - pair seed determinism for Insta/OF metadata and formatted soft/hard outputs; +- pair person, scene, expression, and composition rerolls changing only their + intended soft/hard metadata axes; - pair content rerolls changing soft outfit/teaser content while keeping cast, scene, hard action, and composition axes stable; - pair pose rerolls changing hardcore action metadata while keeping cast, diff --git a/tools/prompt_map_audit.py b/tools/prompt_map_audit.py index fdcd2a7..1b09dc5 100644 --- a/tools/prompt_map_audit.py +++ b/tools/prompt_map_audit.py @@ -94,6 +94,10 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = ( "docs/prompt-pool-routing-map.md", "pair seed determinism for Insta/OF metadata", ), + ( + "docs/prompt-pool-routing-map.md", + "pair person, scene, expression, and composition rerolls", + ), ( "docs/prompt-pool-routing-map.md", "pair content rerolls changing soft outfit/teaser content", diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index ce94d02..5c8f92b 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -1021,6 +1021,47 @@ def _pair_seed_determinism_check(seed: int) -> dict[str, Any]: } +def _pair_seed_reroll_check( + seed: int, + *, + name: str, + reroll_axis: str, + changed_fields: tuple[str, ...], + stable_fields: tuple[str, ...], + base_target: str, +) -> dict[str, Any]: + base = _pair_seed_probe(seed) + base_snapshot = _pair_seed_snapshot(base) + changed = False + changed_seed = None + changed_field_names: list[str] = [] + issues: list[str] = [] + for reroll_seed in range(seed + 1, seed + 16): + rerolled = _pair_seed_probe(seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed) + rerolled_snapshot = _pair_seed_snapshot(rerolled) + field_issues = _same_fields_issues(name, base_snapshot, rerolled_snapshot, stable_fields, reroll_seed) + if field_issues: + issues.extend(field_issues) + break + changed_field_names = [ + field for field in changed_fields if base_snapshot.get(field) != rerolled_snapshot.get(field) + ] + if changed_field_names: + changed = True + changed_seed = reroll_seed + break + if not changed: + issues.append(f"{name} did not change {', '.join(changed_fields)} within 15 attempts") + return { + "name": name, + "base": _row_summary(base.get(f"{base_target}_row") or {}), + "changed": changed, + "changed_seed": changed_seed, + "changed_fields": changed_field_names, + "issues": issues, + } + + def _seed_reroll_check( seed: int, *, @@ -1062,95 +1103,141 @@ def _seed_reroll_check( def _pair_seed_pose_reroll_check(seed: int) -> dict[str, Any]: - name = "pair_seed.pose_reroll" - base = _pair_seed_probe(seed) - base_snapshot = _pair_seed_snapshot(base) - changed = False - changed_seed = None - changed_field_names: list[str] = [] - issues: list[str] = [] - stable_fields = ( - "shared_cast_descriptors", - "soft_cast_descriptor_text", - "hard_cast_descriptor_text", - "soft_scene_text", - "hard_scene_text", - "soft_item", - "soft_composition", - "hard_composition", + return _pair_seed_reroll_check( + seed, + name="pair_seed.pose_reroll", + reroll_axis="pose", + changed_fields=("hard_position_key", "hard_item", "hard_source_role_graph"), + stable_fields=( + "shared_cast_descriptors", + "soft_cast_descriptor_text", + "hard_cast_descriptor_text", + "soft_scene_text", + "hard_scene_text", + "soft_item", + "soft_pose", + "soft_composition", + "hard_composition", + ), + base_target="hardcore", ) - changed_fields = ("hard_position_key", "hard_item", "hard_source_role_graph") - for reroll_seed in range(seed + 1, seed + 16): - rerolled = _pair_seed_probe(seed, reroll_axis="pose", reroll_seed=reroll_seed) - rerolled_snapshot = _pair_seed_snapshot(rerolled) - field_issues = _same_fields_issues(name, base_snapshot, rerolled_snapshot, stable_fields, reroll_seed) - if field_issues: - issues.extend(field_issues) - break - changed_field_names = [ - field for field in changed_fields if base_snapshot.get(field) != rerolled_snapshot.get(field) - ] - if changed_field_names: - changed = True - changed_seed = reroll_seed - break - if not changed: - issues.append("pair pose reroll did not change hard_position_key, hard_item, or hard_source_role_graph within 15 attempts") - return { - "name": name, - "base": _row_summary(base.get("hardcore_row") or {}), - "changed": changed, - "changed_seed": changed_seed, - "changed_fields": changed_field_names, - "issues": issues, - } def _pair_seed_content_reroll_check(seed: int) -> dict[str, Any]: - name = "pair_seed.content_reroll" - base = _pair_seed_probe(seed) - base_snapshot = _pair_seed_snapshot(base) - changed = False - changed_seed = None - changed_field_names: list[str] = [] - issues: list[str] = [] - stable_fields = ( - "shared_cast_descriptors", - "soft_cast_descriptor_text", - "hard_cast_descriptor_text", - "soft_scene_text", - "hard_scene_text", - "hard_item", - "hard_position_key", - "hard_source_role_graph", - "soft_composition", - "hard_composition", + return _pair_seed_reroll_check( + seed, + name="pair_seed.content_reroll", + reroll_axis="content", + changed_fields=("soft_item", "soft_pose"), + stable_fields=( + "shared_cast_descriptors", + "soft_cast_descriptor_text", + "hard_cast_descriptor_text", + "soft_scene_text", + "hard_scene_text", + "hard_item", + "hard_position_key", + "hard_source_role_graph", + "soft_composition", + "hard_composition", + ), + base_target="softcore", + ) + + +def _pair_seed_person_reroll_check(seed: int) -> dict[str, Any]: + return _pair_seed_reroll_check( + seed, + name="pair_seed.person_reroll", + reroll_axis="person", + changed_fields=("shared_cast_descriptors", "soft_cast_descriptor_text", "hard_cast_descriptor_text"), + stable_fields=( + "soft_scene_text", + "hard_scene_text", + "soft_item", + "soft_pose", + "hard_item", + "hard_position_key", + "hard_source_role_graph", + "soft_composition", + "hard_composition", + "soft_expression", + "hard_expression", + ), + base_target="hardcore", + ) + + +def _pair_seed_scene_reroll_check(seed: int) -> dict[str, Any]: + return _pair_seed_reroll_check( + seed, + name="pair_seed.scene_reroll", + reroll_axis="scene", + changed_fields=("soft_scene_text", "hard_scene_text"), + stable_fields=( + "shared_cast_descriptors", + "soft_cast_descriptor_text", + "hard_cast_descriptor_text", + "soft_item", + "soft_pose", + "hard_item", + "hard_position_key", + "hard_source_role_graph", + "soft_composition", + "hard_composition", + "soft_expression", + "hard_expression", + ), + base_target="hardcore", + ) + + +def _pair_seed_expression_reroll_check(seed: int) -> dict[str, Any]: + return _pair_seed_reroll_check( + seed, + name="pair_seed.expression_reroll", + reroll_axis="expression", + changed_fields=("soft_expression", "hard_expression"), + stable_fields=( + "shared_cast_descriptors", + "soft_cast_descriptor_text", + "hard_cast_descriptor_text", + "soft_scene_text", + "hard_scene_text", + "soft_item", + "soft_pose", + "hard_item", + "hard_position_key", + "hard_source_role_graph", + "soft_composition", + "hard_composition", + ), + base_target="hardcore", + ) + + +def _pair_seed_composition_reroll_check(seed: int) -> dict[str, Any]: + return _pair_seed_reroll_check( + seed, + name="pair_seed.composition_reroll", + reroll_axis="composition", + changed_fields=("soft_composition", "hard_composition"), + stable_fields=( + "shared_cast_descriptors", + "soft_cast_descriptor_text", + "hard_cast_descriptor_text", + "soft_scene_text", + "hard_scene_text", + "soft_item", + "soft_pose", + "hard_item", + "hard_position_key", + "hard_source_role_graph", + "soft_expression", + "hard_expression", + ), + base_target="hardcore", ) - changed_fields = ("soft_item", "soft_pose") - for reroll_seed in range(seed + 1, seed + 16): - rerolled = _pair_seed_probe(seed, reroll_axis="content", reroll_seed=reroll_seed) - rerolled_snapshot = _pair_seed_snapshot(rerolled) - field_issues = _same_fields_issues(name, base_snapshot, rerolled_snapshot, stable_fields, reroll_seed) - if field_issues: - issues.extend(field_issues) - break - changed_field_names = [ - field for field in changed_fields if base_snapshot.get(field) != rerolled_snapshot.get(field) - ] - if changed_field_names: - changed = True - changed_seed = reroll_seed - break - if not changed: - issues.append("pair content reroll did not change soft_item or soft_pose within 15 attempts") - return { - "name": name, - "base": _row_summary(base.get("softcore_row") or {}), - "changed": changed, - "changed_seed": changed_seed, - "changed_fields": changed_field_names, - "issues": issues, - } def _seed_axis_checks(seed: int) -> list[dict[str, Any]]: @@ -1192,8 +1279,12 @@ def _seed_axis_checks(seed: int) -> list[dict[str, Any]]: def _pair_seed_checks(seed: int) -> list[dict[str, Any]]: return [ _pair_seed_determinism_check(seed), + _pair_seed_person_reroll_check(seed), + _pair_seed_scene_reroll_check(seed), _pair_seed_content_reroll_check(seed), _pair_seed_pose_reroll_check(seed), + _pair_seed_expression_reroll_check(seed), + _pair_seed_composition_reroll_check(seed), ] diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index f0855c1..d4d9d41 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -7918,7 +7918,7 @@ def smoke_prompt_route_simulation_policy() -> None: _expect(summary.get("cases") == 14, "Prompt route simulation case count changed unexpectedly") _expect(summary.get("coverage_checks") == 2, "Prompt route simulation lost family coverage checks") _expect(summary.get("axis_checks") == 6, "Prompt route simulation lost axis check coverage") - _expect(summary.get("pair_seed_checks") == 3, "Prompt route simulation lost pair seed check coverage") + _expect(summary.get("pair_seed_checks") == 7, "Prompt route simulation lost pair seed check coverage") _expect(summary.get("issues") == 0, f"Prompt route simulation reported issues: {report.get('issues')}") cases = {case.get("name"): case for case in report.get("cases") or []} for route_name in ( @@ -7981,7 +7981,15 @@ def smoke_prompt_route_simulation_policy() -> None: ): _expect(axis_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can reroll") pair_seed_checks = {check.get("name"): check for check in report.get("pair_seed_checks") or []} - for check_name in ("pair_seed.locked_determinism", "pair_seed.content_reroll", "pair_seed.pose_reroll"): + for check_name in ( + "pair_seed.locked_determinism", + "pair_seed.person_reroll", + "pair_seed.scene_reroll", + "pair_seed.content_reroll", + "pair_seed.pose_reroll", + "pair_seed.expression_reroll", + "pair_seed.composition_reroll", + ): check = pair_seed_checks.get(check_name) or {} _expect(check, f"Prompt route simulation lost pair seed check {check_name}") _expect(not check.get("issues"), f"Prompt route simulation pair seed check reported issues: {check_name}") @@ -7989,6 +7997,15 @@ def smoke_prompt_route_simulation_policy() -> None: pair_seed_checks["pair_seed.locked_determinism"].get("changed") is False, "Pair locked determinism check should not be a reroll", ) + for check_name in ( + "pair_seed.person_reroll", + "pair_seed.scene_reroll", + "pair_seed.content_reroll", + "pair_seed.pose_reroll", + "pair_seed.expression_reroll", + "pair_seed.composition_reroll", + ): + _expect(pair_seed_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can reroll") _expect( pair_seed_checks["pair_seed.content_reroll"].get("changed") is True, "Pair content reroll should prove soft outfit/content can reroll while hard action stays locked",