Plan clothing seed axis implementation

This commit is contained in:
2026-07-01 16:06:37 +02:00
parent cddc5d0a4d
commit c0985cddbe
@@ -0,0 +1,854 @@
# Clothing Seed Axis Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a first-class `clothing` seed axis so workflows can keep content, pose, role, person, scene, expression, and composition identical while rerolling only clothing/outfit choices.
**Architecture:** Extend the shared seed policy first, then route clothing selections through `axis_rng(..., "clothing", ...)` in prompt and pair flows. Keep `content` responsible for content item/template choices, use `outfit_seed` as a clothing alias, and keep `content_seed` as a compatibility fallback only when no explicit clothing/outfit seed exists.
**Tech Stack:** Python 3, ComfyUI custom nodes, local smoke tests in `tools/prompt_smoke.py`, scene nodes in `node_scene.py`, seed policy in `seed_config.py`.
---
## File Structure
- Modify `seed_config.py`: add the `clothing` seed axis, aliases, reroll groups, lock config emission, and trace support through the existing generic functions.
- Modify `prompt_builder.py`: expose optional `clothing_seed` and `clothing_seed_mode` through the wrapper around `seed_config.build_seed_config_json`.
- Modify `node_seed_resolution.py`: expose clothing controls in `SxCPSeedControl` and let `SxCPSeedLocker` pick up the new reroll choices from `seed_config`.
- Modify `node_tooltips.py`: add help text for the new manual clothing seed controls and update the seed-locker tooltip.
- Modify `builder_prompt_route.py`: use a clothing RNG for prompt clothing mode selection.
- Modify `pair_rows.py`: use a clothing RNG for primary softcore outfit selection in scene pairs.
- Modify `pair_cast.py`: use a clothing RNG for secondary pair participant outfits.
- Modify `node_scene.py`: map scene layer seed axes to `clothing`, `content_clothing`, and `clothing_pose`.
- Modify `tools/prompt_smoke.py`: add red/green smoke coverage for seed vocabulary, UI inputs, routing, and scene-pair clothes-only rerolls.
---
### Task 1: Shared Seed Vocabulary And UI Surface
**Files:**
- Modify: `tools/prompt_smoke.py`
- Modify: `seed_config.py`
- Modify: `prompt_builder.py`
- Modify: `node_seed_resolution.py`
- Modify: `node_tooltips.py`
- [ ] **Step 1: Write the failing seed vocabulary smoke tests**
In `tools/prompt_smoke.py`, inside `smoke_seed_config_policy()`, after the existing `normalize_reroll_axis("content pose")` assertion, add:
```python
reroll_choices = pb.seed_reroll_axis_choices()
for expected_axis in ("clothing", "content_clothing", "clothing_pose"):
_expect(expected_axis in reroll_choices, f"seed reroll axis choices missing {expected_axis}")
_expect(pb.normalize_reroll_axis("clothing pose") == "clothing_pose", "reroll axis normalizer should accept clothing pose")
_expect(pb.normalize_reroll_axis("content clothing") == "content_clothing", "reroll axis normalizer should accept content clothing")
```
In the same function, replace:
```python
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "bad": "nope"})
_expect(parsed == {"item_seed": 44, "pose_seed": 55}, "seed parser should keep integer-like values only")
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
```
with:
```python
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "outfit_seed": "66", "bad": "nope"})
_expect(
parsed == {"item_seed": 44, "pose_seed": 55, "outfit_seed": 66},
"seed parser should keep integer-like values only",
)
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
_expect(pb._configured_axis_seed(parsed, "clothing") == 66, "clothing axis should honor outfit_seed alias")
_expect(
pb._configured_axis_seed({"content_seed": 77}, "clothing") == 77,
"clothing axis should keep content_seed as a legacy fallback",
)
_expect(
pb._configured_axis_seed({"content_seed": 77, "clothing_seed": 88}, "clothing") == 88,
"clothing_seed should override legacy content_seed fallback",
)
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
```
In the same function, after the existing `locked = json.loads(...)` block and its three assertions, add:
```python
clothing_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing", reroll_seed=777))
_expect(clothing_locked["clothing_seed"] == 777, "clothing reroll should alter clothing seed")
_expect(clothing_locked["content_seed"] == 100, "clothing reroll should leave content locked")
_expect(clothing_locked["pose_seed"] == 100 and clothing_locked["role_seed"] == 100, "clothing reroll should leave pose and role locked")
content_clothing_locked = json.loads(
pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_clothing", reroll_seed=778)
)
_expect(content_clothing_locked["content_seed"] == 778, "content_clothing reroll should alter content seed")
_expect(content_clothing_locked["clothing_seed"] == 778, "content_clothing reroll should alter clothing seed")
_expect(content_clothing_locked["pose_seed"] == 100, "content_clothing reroll should leave pose locked")
clothing_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing_pose", reroll_seed=779))
_expect(clothing_pose_locked["clothing_seed"] == 779, "clothing_pose reroll should alter clothing seed")
_expect(clothing_pose_locked["pose_seed"] == 779 and clothing_pose_locked["role_seed"] == 779, "clothing_pose reroll should alter pose and role seeds")
_expect(clothing_pose_locked["content_seed"] == 100, "clothing_pose reroll should leave content locked")
content_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_pose", reroll_seed=780))
_expect(content_pose_locked["clothing_seed"] == 100, "content_pose reroll should not alter clothing seed")
```
Change the `axis_trace` test block from:
```python
axis_trace = seed_config.axis_seed_trace({"content_seed": 44}, 99, 3, axes=("content", "scene"))
```
to:
```python
axis_trace = seed_config.axis_seed_trace({"content_seed": 44, "clothing_seed": 66}, 99, 3, axes=("content", "clothing", "scene"))
```
Then add this assertion after the existing content seed assertions:
```python
_expect(axis_trace["clothing"]["source"] == "configured", "Seed axis trace lost clothing configured source")
_expect(axis_trace["clothing"]["seed"] == 66, "Seed axis trace lost configured clothing seed")
```
In `tools/prompt_smoke.py`, inside `smoke_node_utility_registration()`, after:
```python
_expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input")
```
add:
```python
_expect("clothing_seed_mode" in seed_inputs, "Seed Control lost clothing seed mode input")
_expect("clothing_seed" in seed_inputs, "Seed Control lost clothing seed input")
```
After the `category_seed_tooltip` assertion, add:
```python
clothing_seed_tooltip = node_tooltips._tooltip_for_input("SxCPSeedControl", "clothing_seed_mode")
_expect("clothing/outfit" in clothing_seed_tooltip, "Node tooltip policy lost Seed Control clothing override")
```
After:
```python
_expect(int(parsed_seed_control.get("content_seed", -1)) >= 0, "Seed Control random mode did not emit resolved seed")
```
add this assertion after updating the call in Step 3:
```python
_expect(parsed_seed_control.get("clothing_seed") == 222, "Seed Control fixed clothing seed changed")
```
- [ ] **Step 2: Run the focused smoke tests and verify they fail for the new behavior**
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
```
Expected: FAIL with `seed reroll axis choices missing clothing`.
Run:
```bash
python tools/prompt_smoke.py --case node_utility_registration --quiet
```
Expected: FAIL with `Seed Control lost clothing seed mode input`.
- [ ] **Step 3: Implement the shared seed axis**
In `seed_config.py`, update the top-level seed definitions:
```python
SEED_AXIS_SALTS = {
"category": 31,
"subcategory": 37,
"content": 41,
"clothing": 41,
"person": 43,
"scene": 47,
"pose": 53,
"role": 57,
"expression": 59,
"composition": 61,
}
SEED_AXIS_ALIASES = {
"category": ("category_seed", "category"),
"subcategory": ("subcategory_seed", "subcategory"),
"content": ("content_seed", "item_seed", "sexual_pose_seed", "content"),
"clothing": ("clothing_seed", "outfit_seed", "wardrobe_seed", "content_seed", "content"),
"person": ("person_seed", "appearance_seed", "cast_seed", "person"),
"scene": ("scene_seed", "scene"),
"pose": ("pose_seed", "sexual_pose_seed", "pose"),
"role": ("role_seed", "role", "pose_seed", "sexual_pose_seed"),
"expression": ("expression_seed", "face_seed", "expression"),
"composition": ("composition_seed", "camera_seed", "composition"),
}
SEED_LOCK_AXES = (
"category",
"subcategory",
"content",
"clothing",
"person",
"scene",
"pose",
"role",
"expression",
"composition",
)
```
In the same file, update `SEED_REROLL_GROUPS`:
```python
SEED_REROLL_GROUPS = {
"none": (),
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"clothing": ("clothing",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
"role": ("role",),
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"content_clothing": ("content", "clothing"),
"clothing_pose": ("clothing", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
```
Update `normalize_reroll_axis()` aliases:
```python
aliases = {
"contentpose": "content_pose",
"contentclothing": "content_clothing",
"clothingpose": "clothing_pose",
"scenepose": "scene_pose",
}
```
Update `build_seed_config_json()` in `seed_config.py` by adding parameters between `content_seed` and `person_seed`:
```python
clothing_seed: int = -1,
```
and between `content_seed_mode` and `person_seed_mode`:
```python
clothing_seed_mode: str = "auto",
```
Then add the emitted field after `content_seed`:
```python
"clothing_seed": axis_seed(clothing_seed, clothing_seed_mode),
```
In `prompt_builder.py`, update `build_seed_config_json()` with the same optional `clothing_seed` and `clothing_seed_mode` parameters, and pass them to `seed_policy.build_seed_config_json(...)`:
```python
clothing_seed=clothing_seed,
clothing_seed_mode=clothing_seed_mode,
```
In `node_seed_resolution.py`, update `SxCPSeedControl.SEED_AXES`:
```python
SEED_AXES = (
"category",
"subcategory",
"content",
"clothing",
"person",
"scene",
"pose",
"role",
"expression",
"composition",
)
```
Update `SxCPSeedControl.build(...)` by adding `clothing_seed_mode, clothing_seed` after `content_seed`, and pass both into `build_seed_config_json(...)`:
```python
clothing_seed=clothing_seed,
clothing_seed_mode=clothing_seed_mode,
```
In `tools/prompt_smoke.py`, update the `seed_control().build(...)` call in `smoke_node_utility_registration()` by inserting these two arguments after the current content seed pair:
```python
"fixed",
222,
```
In `node_tooltips.py`, update `NODE_INPUT_TOOLTIPS["SxCPSeedControl"]` with:
```python
"clothing_seed_mode": "Controls clothing/outfit selection separately from content item selection.",
"clothing_seed": "Seed used when clothing_seed_mode is fixed or auto with a non-negative value.",
```
Update `NODE_INPUT_TOOLTIPS["SxCPSeedLocker"]["reroll_axis"]` to mention clothing:
```python
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use clothing for outfit-only rerolls, pose for sexual pose, scene for location, person for appearance.",
```
- [ ] **Step 4: Run the focused smoke tests and verify they pass**
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
```
Expected: `OK: smoke passed (1 cases).`
Run:
```bash
python tools/prompt_smoke.py --case node_utility_registration --quiet
```
Expected: `OK: smoke passed (1 cases).`
- [ ] **Step 5: Commit Task 1**
Run:
```bash
git --git-dir=.git-real --work-tree=. add seed_config.py prompt_builder.py node_seed_resolution.py node_tooltips.py tools/prompt_smoke.py
git --git-dir=.git-real --work-tree=. commit -m "Add clothing seed axis vocabulary"
```
---
### Task 2: Route Prompt And Pair Clothing Through The Clothing Axis
**Files:**
- Modify: `tools/prompt_smoke.py`
- Modify: `builder_prompt_route.py`
- Modify: `pair_rows.py`
- Modify: `pair_cast.py`
- [ ] **Step 1: Write the failing prompt-routing smoke test**
In `tools/prompt_smoke.py`, inside `smoke_seed_config_policy()`, after the existing `pose_changed` assertion block, add:
```python
clothes_base_seed = 52001
clothes_base_config = json.loads(pb.build_seed_lock_config_json(base_seed=clothes_base_seed))
def clothes_row(clothing_seed: int) -> dict[str, Any]:
seed_config_for_row = dict(clothes_base_config)
seed_config_for_row["clothing_seed"] = clothing_seed
return _prompt_row(
name=f"seed_config_policy_clothing_seed_{clothing_seed}",
category="woman",
subcategory="",
seed=clothes_base_seed,
seed_config=seed_config_for_row,
clothing="random",
minimal_clothing_ratio=0.5,
character_cast=_character_cast(),
location_config=_coworking_location_config(),
)
clothes_locked_a = clothes_row(53001)
clothes_changed = False
for clothing_seed in range(53002, 53100):
clothes_candidate = clothes_row(clothing_seed)
_expect(
clothes_candidate.get("scene_text") == clothes_locked_a.get("scene_text"),
"clothing reroll should keep scene text stable",
)
_expect(
clothes_candidate.get("pose") == clothes_locked_a.get("pose"),
"clothing reroll should keep pose stable",
)
_expect(
clothes_candidate.get("cast_descriptor_text") == clothes_locked_a.get("cast_descriptor_text"),
"clothing reroll should keep cast descriptors stable",
)
if clothes_candidate.get("clothing") != clothes_locked_a.get("clothing"):
clothes_changed = True
break
_expect(clothes_changed, "clothing_seed reroll should change prompt clothing mode")
```
- [ ] **Step 2: Run the focused smoke test and verify it fails for the current routing**
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
```
Expected: FAIL with `clothing_seed reroll should change prompt clothing mode`.
- [ ] **Step 3: Implement clothing RNG routing in normal prompt rows**
In `builder_prompt_route.py`, inside `build_prompt_result(...)`, replace:
```python
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
```
with:
```python
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
clothing_rng = deps.axis_rng(parsed_seed_config, "clothing", seed, row_number)
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
```
Then replace:
```python
clothing = deps.pick_clothing_mode(content_rng, clothing, minimal_ratio)
```
with:
```python
clothing = deps.pick_clothing_mode(clothing_rng, clothing, minimal_ratio)
```
- [ ] **Step 4: Run the focused smoke test and verify it passes**
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
```
Expected: `OK: smoke passed (1 cases).`
- [ ] **Step 5: Write the failing pair-routing smoke tests**
In `tools/prompt_smoke.py`, inside `smoke_node_scene_chain_registration()`, find:
```python
soft_content_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"softcore_branch",
"fixed",
6679,
"content",
"same_for_all_rows",
"replace_layer",
)[0]
```
Replace it with:
```python
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"softcore_branch",
"fixed",
6679,
"clothing",
"same_for_all_rows",
"replace_layer",
)[0]
```
In the same block, replace `seed_options=soft_content_seed_options` with:
```python
seed_options=soft_clothing_seed_options,
```
Update the expected failure message from:
```python
"Scene softcore branch content seed fixture no longer selects the expected outfit",
```
to:
```python
"Scene softcore branch clothing seed fixture no longer selects the expected outfit",
```
After the existing `soft_pose_pair` assertions and before the choice-board block, add:
```python
def _soft_clothing_pair(soft_clothing_seed: int) -> dict[str, Any]:
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"softcore_branch",
"fixed",
soft_clothing_seed,
"clothing",
"same_for_all_rows",
"replace_layer",
)[0]
soft_scene_clothing, hard_scene_clothing, _summary, _metadata = nodes["SxCPSceneBranchPair"]().build(
scene,
"same_creator_same_room",
"hybrid",
branch_options=branch_options,
seed_options=soft_clothing_seed_options,
)
soft_scene_clothing = nodes["SxCPSoftcoreBranchOptions"]().build(
soft_scene_clothing,
"same_as_hardcore",
"lingerie_tease",
True,
0.45,
"from_camera_config",
"compact",
"",
branch_options=branch_options,
seed_options=soft_clothing_seed_options,
)[0]
hard_scene_clothing = nodes["SxCPHardcoreBranchOptions"]().build(
hard_scene_clothing,
"couple",
1,
1,
"hardcore",
True,
0.85,
"partially_removed",
"from_camera_config",
"compact",
"balanced",
"",
branch_options=branch_options,
)[0]
return json.loads(nodes["SxCPScenePairOutput"]().build(soft_scene_clothing, hard_scene_clothing)[7])
soft_clothing_pairs = [_soft_clothing_pair(seed) for seed in (6677, 6678, 6679, 6680)]
soft_clothing_items = {pair.get("softcore_row", {}).get("item") for pair in soft_clothing_pairs}
soft_clothing_poses = {pair.get("softcore_row", {}).get("pose") for pair in soft_clothing_pairs}
soft_clothing_hard_states = {pair.get("hardcore_clothing_state") for pair in soft_clothing_pairs}
_expect(len(soft_clothing_items) > 1, "Softcore branch clothing reroll should change softcore outfit")
_expect(len(soft_clothing_hard_states) > 1, "Softcore branch clothing reroll should change inherited hard clothing")
_expect(len(soft_clothing_poses) == 1, "Softcore branch clothing reroll should keep softcore pose stable")
for expected_seed, clothing_pair in zip((6677, 6678, 6679, 6680), soft_clothing_pairs):
soft_seed_config = clothing_pair.get("softcore_row", {}).get("seed_config") if isinstance(clothing_pair.get("softcore_row"), dict) else {}
hard_seed_config = clothing_pair.get("hardcore_row", {}).get("seed_config") if isinstance(clothing_pair.get("hardcore_row"), dict) else {}
_expect(
soft_seed_config.get("clothing_seed") == expected_seed,
"Softcore branch clothing seed did not reach softcore generator seed config",
)
_expect(
soft_seed_config.get("content_seed") != expected_seed,
"Softcore branch clothing seed should not overwrite content seed",
)
_expect(
hard_seed_config.get("clothing_seed") != expected_seed,
"Softcore branch clothing seed leaked into hardcore generator seed config",
)
```
In the `content_pair` assertions, add:
```python
_expect(
content_hard_seed_config.get("clothing_seed") != 8899,
"Hardcore branch content_pose reroll should not reach hardcore clothing seed",
)
```
- [ ] **Step 6: Run the scene-chain smoke test and verify it fails for the missing scene axis/routing**
Run:
```bash
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
```
Expected: FAIL with either `Scene softcore branch clothing seed fixture no longer selects the expected outfit` or `Softcore branch clothing reroll should change softcore outfit`.
- [ ] **Step 7: Implement clothing RNG routing in pair rows**
In `pair_rows.py`, inside `build_insta_pair_rows_result(...)`, replace:
```python
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
```
with:
```python
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
soft_clothing_rng = axis_rng(soft_seed_config, "clothing", seed, row_number + 311)
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
```
Then replace:
```python
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_content_rng)
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_content_rng, softcore_level_key)
```
with:
```python
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_clothing_rng)
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_clothing_rng, softcore_level_key)
```
In `pair_cast.py`, inside `softcore_partner_styling(...)`, replace:
```python
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
```
with:
```python
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
clothing_rng = axis_rng(seed_config, "clothing", seed, row_number + 421)
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
```
Then replace both `slot_softcore_outfit(..., content_rng)` calls with `slot_softcore_outfit(..., clothing_rng)`, and replace both outfit `choose(content_rng, ...)` calls with `choose(clothing_rng, ...)`.
- [ ] **Step 8: Run focused smoke tests and verify Task 2 behavior passes**
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
```
Expected: `OK: smoke passed (1 cases).`
Run:
```bash
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
```
Expected at this point: FAIL only on missing scene layer mapping, with a message involving the softcore branch clothing seed.
- [ ] **Step 9: Commit Task 2**
If `seed_config_policy` passes and `node_scene_chain_registration` now fails only because scene layer axes do not apply `clothing`, commit the prompt and pair routing work:
```bash
git --git-dir=.git-real --work-tree=. add builder_prompt_route.py pair_rows.py pair_cast.py tools/prompt_smoke.py
git --git-dir=.git-real --work-tree=. commit -m "Route clothing choices through clothing seed"
```
---
### Task 3: Scene Layer Clothing Axis Mapping
**Files:**
- Modify: `node_scene.py`
- Modify: `tools/prompt_smoke.py`
- [ ] **Step 1: Write the failing scene-axis smoke assertions**
In `tools/prompt_smoke.py`, inside `smoke_node_scene_chain_registration()`, replace:
```python
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "content", "same_for_all_rows", "replace_layer")[0]
```
with:
```python
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "clothing", "same_for_all_rows", "replace_layer")[0]
```
After:
```python
_expect(json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("seed") == 9981, "Scene Wardrobe seed options did not write seed trace")
```
add:
```python
_expect(
json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("axes") == ["clothing"],
"Scene Wardrobe seed options should target clothing axis",
)
```
- [ ] **Step 2: Run scene-chain smoke and verify it fails for missing scene layer clothing mapping**
Run:
```bash
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
```
Expected: FAIL with a message involving wardrobe or softcore branch clothing axis mapping.
- [ ] **Step 3: Implement scene layer clothing mappings**
In `node_scene.py`, update `SCENE_LAYER_SEED_AXES`:
```python
SCENE_LAYER_SEED_AXES = {
"cast": ("category",),
"character": ("person",),
"wardrobe": ("clothing",),
"location": ("scene",),
"set_dressing": ("scene",),
"blocking": ("pose",),
"action": ("pose", "role"),
"performance": ("expression",),
"camera": ("composition",),
"composition": ("composition",),
"lighting": ("composition",),
"softcore_branch": ("clothing", "pose", "role"),
"hardcore_branch": ("pose", "role"),
}
```
In the same file, update `SCENE_REROLL_GROUPS`:
```python
SCENE_REROLL_GROUPS = {
"none": (),
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"clothing": ("clothing",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
"role": ("role",),
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"content_clothing": ("content", "clothing"),
"clothing_pose": ("clothing", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
```
- [ ] **Step 4: Run focused smoke tests and verify they pass**
Run:
```bash
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
```
Expected: `OK: smoke passed (1 cases).`
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
```
Expected: `OK: smoke passed (1 cases).`
Run:
```bash
python tools/prompt_smoke.py --case node_utility_registration --quiet
```
Expected: `OK: smoke passed (1 cases).`
- [ ] **Step 5: Commit Task 3**
Run:
```bash
git --git-dir=.git-real --work-tree=. add node_scene.py tools/prompt_smoke.py
git --git-dir=.git-real --work-tree=. commit -m "Map scene clothing seeds to clothing axis"
```
---
### Task 4: Final Verification And Push
**Files:**
- No production file edits expected.
- Verify all files touched by Tasks 1-3.
- [ ] **Step 1: Run compilation checks**
Run:
```bash
python -m py_compile seed_config.py prompt_builder.py node_seed_resolution.py node_tooltips.py builder_prompt_route.py pair_rows.py pair_cast.py node_scene.py tools/prompt_smoke.py
```
Expected: exit code 0.
- [ ] **Step 2: Run focused smoke tests**
Run:
```bash
python tools/prompt_smoke.py --case seed_config_policy --quiet
python tools/prompt_smoke.py --case node_utility_registration --quiet
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
```
Expected for each command: `OK: smoke passed (1 cases).`
- [ ] **Step 3: Run the full smoke suite**
Run:
```bash
python tools/prompt_smoke.py --quiet
```
Expected: all cases pass except the known unrelated `krea2_prompt_guide_policy` failure if it is still present. If any new failure mentions seed policy, scene layer seed, clothing state, prompt routing, pair rows, or node utility registration, fix it before committing or pushing.
- [ ] **Step 4: Check git status**
Run:
```bash
git --git-dir=.git-real --work-tree=. status --short --branch
git --git-dir=.git-real --work-tree=. log -5 --oneline
```
Expected: branch contains the three implementation commits from this plan and no unstaged edits.
- [ ] **Step 5: Push the branch**
Run:
```bash
git --git-dir=.git-real --work-tree=. push
```
Expected: push succeeds to `origin/hardcore-interaction-expansion`.
---
## Self-Review
- Spec coverage: Task 1 covers seed vocabulary, aliases, seed-lock config, seed trace, seed-control UI, and tooltips. Task 2 covers normal prompt clothing mode routing, primary softcore outfit routing, partner outfit routing, and legacy content fallback. Task 3 covers scene layer mappings, wardrobe axis behavior, softcore branch clothing-only rerolls, hard-branch continuity, and `content_pose` not touching clothing. Task 4 covers compilation, focused smoke tests, full smoke, status, and push.
- Scope check: this plan avoids rewriting category data and keeps custom content item selection on the `content` axis, matching the spec's out-of-scope section.
- Type consistency: all new seed keys use `clothing_seed`; all new axis names use `clothing`, `content_clothing`, and `clothing_pose`; compatibility aliases use `outfit_seed`, `wardrobe_seed`, and fallback `content_seed`.