Validate pair seed axis rerolls

This commit is contained in:
2026-06-27 19:41:10 +02:00
parent 007386aae3
commit 3c7ccbb711
5 changed files with 204 additions and 90 deletions
+4 -4
View File
@@ -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:
+2
View File
@@ -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,
+4
View File
@@ -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",
+155 -64
View File
@@ -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,13 +1103,11 @@ 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] = []
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",
@@ -1076,44 +1115,20 @@ def _pair_seed_pose_reroll_check(seed: int) -> dict[str, Any]:
"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] = []
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",
@@ -1125,32 +1140,104 @@ def _pair_seed_content_reroll_check(seed: int) -> dict[str, Any]:
"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),
]
+19 -2
View File
@@ -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",