# Prompt Architecture Improvement Plan

This is a working research note for organizing the prompt builder around the
routing map in `docs/prompt-pool-routing-map.md`.

## Current Branch Additions

The current branch adds two major surfaces:

- `SxCP Krea2 Resolution Selector` in `node_seed_resolution.py`, with README
  notes.
- Expanded hardcore interaction/manual/action pools in
  `categories/sexual_poses.json`,
  `categories/expression_composition_pools.json`, `prompt_builder.py`, and
  `krea_formatter.py`.

The map audit currently sees:

- 15 sexual pose subcategories.
- 94 sexual pose item templates.
- 23 expression pools.
- 24 composition pools.
- A new Krea2 resolution node with width/height/API aspect outputs.
- Registered route policy validation, so action/position families stay covered
  by SDXL family tags, caption labels, and SDXL incompatibility-filter keys.
- Route simulation family coverage, so representative generated rows exercise
  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 and person/scene/content/pose/expression/
  composition reroll behavior.
- Formatter route traces expose selected metadata fields, so Krea2, SDXL, and
  caption outputs can be debugged by category, action/position family, selected
  pair side, scene profile, position keys, and POV labels instead of only
  proving that a metadata branch was used.
- Insta/OF side-target training captions no longer prepend shared cast
  descriptors when the selected side row already emits its own cast prose, and
  route simulation flags repeated cast descriptors.
- Route simulation now has an opt-in multi-seed sweep, and the smoke suite runs
  a three-seed sweep so representative route/noise checks are not proven by one
  lucky seed only.
- Route simulation now emits a `quality` summary that groups route health by
  target, action family, and position family, separates route issues from
  coverage/seed-check issues, buckets issue types, and reports weakest cases so
  future prompt-logic passes can target the worst path first.
- Map audit now fails when a registered ComfyUI node display name is missing
  from the route map or README, so utility nodes cannot silently drift out of
  user-facing documentation.

## Architectural Finding

The project has a good functional map, but ownership is still mixed inside large
files:

- `prompt_builder.py` owns selection, character resolution, role graph logic,
  camera adaptation, pair assembly, and some final string cleanup.
- `krea_formatter.py` owns metadata parsing, cast naturalization, sexual action
  rewriting, POV rewriting, clothing cleanup, camera preservation, fallback
  parsing, and final prose assembly.
- `sdxl_formatter.py` owns tag assembly and style/quality presets.
- `caption_naturalizer.py` owns training-caption prose.
- Category JSON files own scalable pool content, but Python still owns several
  compatibility and role-graph decisions.

The biggest maintainability risk is not the number of pools. The risk is that
selection, semantic rewriting, and final text hygiene are too interleaved. When a
prompt has wrong text, it is easy to patch the wrong layer.

## First Refactor Boundary

Generic text hygiene now has one home:

- `prompt_hygiene.py`

It should only handle route-agnostic cleanup:

- whitespace and punctuation normalization;
- empty field-label removal;
- repeated trigger prefix cleanup;
- duplicate comma-list item removal;
- route-agnostic negative-prompt merge/dedupe;
- adjacent duplicate sentence cleanup;
- simple dangling connector cleanup.

It must not make semantic decisions such as sexual action positioning, POV
geometry, clothing state, or model-specific tag weighting. Those stay in the
route-specific owner. It also preserves ordinary words such as `composition`
inside normal sentences; empty field-label cleanup is limited to standalone
labels.

Formatter input/fallback parsing now has one home:

- `formatter_input.py`

It owns route-neutral parsing shared by Krea2, SDXL, and natural-caption
routes:

- input-hint choice lists and normalization for `auto`, `metadata_json`, and
  route-specific text modes;
- whitespace and punctuation normalization before formatter parsing;
- JSON row detection from `metadata_json` or source text;
- trigger-prefix stripping with route-specific trigger candidate lists;
- `Avoid:` positive/negative splitting for fallback text;
- the shared prompt field-label inventory and extraction such as `Setting:`,
  `Sexual scene:`, `Camera control:`, or `Composition:`;
- fallback field-label stripping for tag/text routes that need label-free body
  text;
- row-value fallback from metadata fields to labeled prompt text.

It must not make formatter-style decisions. Krea prose, SDXL tags, and training
caption sentence shape stay in their formatter modules.

Formatter detail-level handling now has one home:

- `formatter_detail.py`

It owns route-neutral prose detail levels, node choice lists, normalization, and
the concise/balanced/dense inclusion gate used by Krea2 and natural-caption
routes. It must not own route-specific style controls such as Krea photographic
mode or caption style-tail policy.

Formatter target handling now has one home:

- `formatter_target.py`

It owns route-neutral target normalization for `auto`, `single`, `softcore`,
and `hardcore`, including node choice lists and pair-side semantics.
Single-output formatters select the softcore side for pair `auto`/`single`
targets, while caption pair routing can still include both sides for combined
training captions.

Shared hardcore phrase cleanup now has one home:

- `hardcore_text_cleanup.py`

It owns environment-anchor normalization used by both prompt generation and
Krea formatting, including malformed surface joins and bed/sheet/couch anchors
that should become model-neutral body-support language. It must stay
route-neutral: no Krea prose, no SDXL tags, and no category selection logic.

Current integration points:

- `prompt_builder.build_prompt`
- `prompt_builder.build_insta_of_pair`
- `krea_formatter.format_krea2_prompt`
- `sdxl_formatter.format_sdxl_prompt`
- `caption_naturalizer.naturalize_caption`

## Target Organization

### Generation Layer

Owner: `prompt_builder.py` plus `categories/*.json`.

Keep here:

- category/subcategory/item selection;
- seed axis routing;
- character slot/profile resolution;
- scene/expression/composition pool selection;
- role graph creation from structured category axes;
- metadata row construction.

Move or isolate later:

- pair assembly helpers that still live in `prompt_builder.py`.

Already isolated:

- single-prompt builder orchestration, including input normalization, seed-axis
  setup, built-in/custom row routing, legacy location/composition handling,
  camera application, and final prompt-row normalization, lives in
  `builder_prompt_route.py`; `prompt_builder.py` keeps the public wrapper.
- config-driven prompt-builder request parsing, helper-node config mapping, and
  direct `build_prompt` kwarg assembly live in `builder_config_route.py`;
  `prompt_builder.py` keeps the public wrapper.
- JSON category loading, subcategory normalization, named scene/expression/
  composition pool loading, cast compatibility filtering, exact subcategory
  lookup, and inheritance-based pool merging live in `category_library.py`.
- JSON `pool_extensions`, legacy pool patching, built-in category choice lists,
  and category/subcategory UI choices live in `category_extensions.py`.
- object-style item-template metadata extraction, action/position family
  normalization, position-key normalization, and metadata audit errors live in
  `category_template_metadata.py`.
- row item selection, weighted item/pair choice, item-template axis filling,
  and oral/outercourse/anal axis compatibility filters live in `row_item.py`;
  `prompt_builder.py` keeps public delegate wrappers.
- outercourse action-kind classification for boobjob, testicle-sucking,
  penis-licking, handjob, and footjob lives in `outercourse_action_policy.py`
  and is shared by row item filtering, role graphs, and Krea action cleanup.
- row category/subcategory/item route resolution lives in
  `row_category_route.py` behind `CategoryItemRoute`, covering hardcore
  position-category filtering, cast-count adjustment, pose-vs-content seed-axis
  choice, item metadata collection, legacy dict compatibility, and
  pose-category item sanitizing; `prompt_builder.py` keeps public delegate
  wrappers.
- row prompt/caption template selection, safe formatting, default prompt
  templates, configured-cast descriptor insertion, and POV directive insertion
  live in `row_rendering.py`; `prompt_builder.py` keeps compatibility aliases.
- row action/position route metadata resolution lives in
  `row_route_metadata.py` behind `ActionPositionRoute`, covering template
  metadata precedence, inferred position-key merging, legacy dict
  compatibility, and source action-family fallback; `prompt_builder.py` keeps
  public delegate wrappers.
- built-in legacy row generation, auto-weighted/auto-full selection, row mode
  randomization, ratio clamps, and expression-intensity randomization live in
  `row_generation.py`; `prompt_builder.py` keeps public delegate wrappers.
- category/cast route preset schemas, config JSON builders, choice lists, and
  parsers live in `category_cast_config.py`; `prompt_builder.py` keeps public
  delegate wrappers for existing nodes and tests.
- generation-time cast count phrases, configured-cast context metadata,
  character-slot label assignment, scene-kind labels, cast-summary wording, and
  couple count normalization live in `cast_context.py`; `prompt_builder.py`
  keeps delegate wrappers where existing generation paths still call the old
  helper names.
- row subject-context routing for single, couple, configured-cast, group, and
  layout subjects lives in `subject_context.py`; it combines appearance policy,
  cast metadata, and generator subject pools behind one row-facing entry point.
- row subject route orchestration, character slot/profile precedence,
  configured-cast POV labels, visible cast descriptor collection, and
  descriptor prompt cleanup live in `row_subject_route.py`;
  `prompt_builder.py` keeps a public delegate wrapper.
- ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter
  parsing, and ethnicity normalization live in `filter_config.py`; character
  routes and builder filters use `prompt_builder.py` delegate wrappers.
- character choice lists, descriptor detail/presence/slot-seed normalization,
  characteristic-list JSON builders/parsers, eye labels, hair config
  builders/parsers, and hair phrase helpers live in `character_config.py`;
  `prompt_builder.py` keeps public delegate wrappers.
- character slot JSON construction, character-cast parsing, slot normalization,
  slot summary text, slot expression override policy, slot seed helpers, and
  slot figure/ethnicity normalization live in `character_slot.py`;
  `prompt_builder.py` keeps public delegate wrappers.
- generation-time subject appearance selection, normalized-slot context
  resolution, slot hair/outfit/clothing selection, character-context row
  application, and character-slot-to-profile-row conversion live in
  `character_appearance.py`; `prompt_builder.py` keeps public delegate wrappers.
- character manual-detail config, profile name/path policy, profile JSON
  normalization, descriptor assembly, save/load/rename/delete operations,
  fallback profile loading, and context override application live in
  `character_profile.py`; `prompt_builder.py` only bridges generated slot rows
  into profile saves.
- generation profile presets, override normalization, trigger policy, and
  profile config parsing live in `generation_profile_config.py`;
  `prompt_builder.py` keeps public delegate wrappers.
- location/composition config presets, themed location packs, custom
  location/composition entry parsing, merge behavior, and config parsing live
  in `location_config.py`; built-in row location/composition config
  application, source metadata, and prompt/caption rewrites live in
  `row_location.py`.
- row scene/expression/pose/composition pool routing, category inheritance,
  runtime location/composition pool overrides, and generator fallback pool
  selection live in `row_pools.py`; `prompt_builder.py` keeps public delegate
  wrappers.
- row scene/pose/expression/composition axis selection lives in
  `row_prompt_axes.py` behind `PromptAxesRoute`, covering compatible-entry
  filtering, expression-disabled handling, per-character expression promotion,
  legacy dict compatibility, POV composition adaptation, and pose-category
  environment sanitizing; `prompt_builder.py` keeps public delegate wrappers.
- row prompt/caption text-field resolution, prompt/caption template selection,
  safe formatting, configured-cast descriptor insertion, and POV directive
  insertion live in `row_rendering.py`; `prompt_builder.py` keeps public
  delegate wrappers.
- row role-graph route sequencing lives in `row_role_graph.py`, covering
  hardcore source role graph construction, pose-category environment-anchor
  cleanup, and POV role-graph rewriting before prompt axes and formatter
  metadata consume the graph.
- row expression text cleanup, expression route resolution, expression
  intensity weighting, character-slot/cast expression override resolution, and
  per-character expression picking plus action-aware character-expression
  sanitizing live in `row_expression.py`; `prompt_builder.py` keeps public
  delegate wrappers.
- hardcore position/action-filter choices, selected-position normalization,
  config JSON builders/parsers, focus-policy toggles, subcategory allow-list
  policy, position-key detection, category filtering, and item-template/axis
  filtering live in `hardcore_position_config.py`.
- hardcore configured-cast role graph generation lives in
  `hardcore_role_graphs.py`; row generation reaches it through
  `row_role_graph.py` after item/axis metadata is selected.
- fallback role graph wording lives in `hardcore_role_fallback.py`, covering
  solo rows, women-only rows, men-only rows, mixed group fallbacks, and support
  partner sentences.
- interaction-style role graph wording lives in `hardcore_role_interaction.py`,
  covering foreplay, manual stimulation, body worship, clothing transitions,
  dominant guidance, camera performance, aftercare, and group coordination.
- outercourse-specific role graph wording has started moving into action-family
  modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking,
  penis-licking, handjob, and footjob body geometry, keyed by
  `outercourse_action_policy.py`.
- oral-specific role graph wording lives in `hardcore_role_oral.py`, including
  direct POV viewer phrasing for kneeling, face-sitting, sixty-nine,
  edge-supported, side-lying, chair, standing, and reclining oral positions.
- penetration-specific role graph wording lives in
  `hardcore_role_penetration.py`, covering the main vaginal penetration
  position families while Krea POV rewriting keeps first-person geometry stable.
- anal/double-contact role graph wording lives in `hardcore_role_anal.py`,
  covering rear-entry anal variants and front/back double-contact source
  geometry.
- climax role graph wording lives in `hardcore_role_climax.py`, covering
  ejaculation aftermath placement for face/body/ass, lap, open-thigh,
  side-lying, and front/back group layouts.
- camera option schema, orbit/Qwen translation, config parsing, camera
  directive text, and camera caption text live in `camera_config.py`;
  camera-scene prose and contextual scene composition mutation for coworking,
  library, and semi-public profiles live in `scene_camera_adapters.py`;
  row-level camera insertion, subject-kind detection, and POV suppression live
  in `row_camera.py`.
- shared POV slot detection, label merging/filtering, builder-side POV
  directives, source role-graph viewer replacement, and shared composition
  cleanup live in `pov_policy.py`; prompt builder and Krea POV routes delegate
  to it.
- shared hardcore environment-anchor cleanup lives in
  `hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
  reaches formatter routes.
- shared hardcore action metadata lives in `hardcore_action_metadata.py`; custom
  rows now emit `action_family`, `position_family`, `position_key`, and
  `position_keys` so formatter routing and debugging do less keyword guessing.
  Krea, SDXL, and training-caption routes consume these fields when present.
- shared row route metadata readers live in `route_metadata.py`, covering
  normalized action family, position family/keys, and route-specific formatter
  hints for Krea, SDXL, and training-caption routes. Position keys are strict
  by default, while SDXL can opt into legacy unknown key tags for compatibility.
- final row and pair text normalization lives in `row_normalization.py`,
  covering trigger prepending, extra-positive append, negative merge/dedupe,
  caption-part joining, embedded soft/hard row output synchronization, and row
  sanitation before metadata leaves generation. It also copies side-specific
  pair metadata, such as soft partner styling and hardcore clothing/detail
  state, plus shared cast descriptors, onto the embedded soft/hard rows.
- final custom-row assembly now lives in `row_assembly.py` behind
  `CustomRowAssemblyRequest`, covering render context population,
  prompt/caption rendering delegation, row-base indexing, row metadata copying,
  configured-cast count metadata, profile/slot metadata, and
  disabled-expression cleanup.

### Pair / Adapter Layer

Owner today: `pair_builder.py`; `prompt_builder.build_insta_of_pair` is the
public wrapper used by the node layer.

Keep here:

- the public wrapper signature and dependency bridge needed by existing nodes
  and tests.

Already isolated:

- Insta/OF option normalization, softcore category/outfit/pose pools, partner
  outfit pools, clothing-continuity labels, negatives, and hardcore cast count
  policy, plus hardcore detail-density directive text, live in
  `pair_options.py`; `prompt_builder.py` keeps public delegate wrappers for
  existing nodes and tests.
- pair route sequencing now lives in `pair_builder.py` behind
  `InstaPairBuildRequest` and `InstaPairBuildDependencies`, covering
  option/filter/seed/cast parsing handoff, soft/hard row orchestration, cast
  context, camera route, clothing route, and final output assembly delegation.
- soft/hard row creation lives in `pair_rows.py` behind `InstaPairRowsRoute`,
  including softcore expression override resolution, Woman A slot context
  application, soft outfit/pose overrides, POV row fields, hardcore row
  creation, and legacy dict compatibility.
- pair-level cast/display context lives in `pair_cast.py`, including descriptor
  prose, descriptor-entry assembly, shared descriptors, cast-label cleanup,
  same-cast softcore descriptor text, partner styling, platform and level
  labels, softcore cast presence text, and hard cast summary text.
- shared softcore pair prose for solo/same-cast/POV presence, caption side
  wording, and creator-shot teaser directives lives in
  `softcore_text_policy.py`; pair, Krea, and caption routes delegate to it.
- pair-level camera routing lives in `pair_camera.py` behind
  `InstaPairCameraRoute`, including soft/hard camera config selection,
  same-as-softcore mode, camera-detail override, same-room hard scene
  continuity, camera-aware composition mutation, POV camera suppression,
  row/root camera metadata synchronization, and legacy dict compatibility.
- pair-level clothing policy lives in `pair_clothing.py` behind
  `HardcorePairClothingRoute`, including clothing sentence formatting,
  body-exposure scene cleanup, action-aware body-access flags, conflicting
  outfit-piece cleanup, default visible-men clothing, character-clothing
  override handling, hardcore clothing continuity, final root clothing-state
  assembly, and legacy dict compatibility.
- final pair output assembly lives in `pair_output.py`, including soft/hard
  prompt strings, trigger preservation, negatives, captions, and root metadata
  shape; the final cleanup step is delegated to `row_normalization.py`.
  Embedded soft/hard rows are synchronized to the final pair prompt, caption,
  and negative outputs during normalization so serialized pair metadata does
  not carry stale standalone row text. Side-specific structured fields are
  synchronized there too, including soft partner styling, hardcore clothing
  continuity metadata, and shared cast descriptors for same-cast caption and
  formatter routes.

### Krea2 Formatter Path

Owner: `krea_formatter.py`.

Keep here:

- Krea prose style;
- Krea top-level route orchestration;
- camera-scene preservation;
- fallback text parsing.

Already isolated:

- `krea_format_route.py` owns top-level Krea dispatch, including option
  normalization, metadata-vs-text input selection, single-vs-pair branching,
  shared target normalization via `formatter_target.py`, extra
  positive/negative merging, final prose hygiene, and output shape;
  `krea_formatter.py` keeps the public wrapper.
- `krea_configured_cast_formatter.py` owns normal metadata configured-cast
  Krea prose assembly behind `KreaConfiguredCastRequest`,
  `KreaConfiguredCastDependencies`, and `KreaConfiguredCastPrompt`;
  `krea_formatter.py` keeps configured-cast detection and compatibility
  wrapper helpers.
- `krea_normal_formatter.py` owns normal metadata single/couple/generic Krea
  prose assembly behind `KreaNormalRowRequest`, `KreaNormalRowDependencies`,
  and `KreaNormalRowPrompt`; `krea_formatter.py` keeps route selection.
- `krea_row_fields.py` owns shared normal-row Krea field extraction for item,
  scene, pose, expression, composition/source-composition, camera, and style so
  normal and configured-cast Krea routes cannot drift independently.
- `krea_pair_formatter.py` owns Insta/OF pair soft/hard Krea prose assembly
  behind `KreaPairFormatRequest`, `KreaPairFormatDependencies`, and
  `KreaPairPrompts`; `krea_formatter.py` keeps the `_insta_pair_to_krea`
  compatibility wrapper.
- `krea_cast.py` owns cast descriptor parsing, cast labels, cast prose, label
  joining, natural cast descriptor text, and label replacement for formatter
  routes, including the caption naturalizer's cast metadata path.
- `krea_clothing.py` owns clothing-state cleanup and action-aware body-access
  wording for formatter routes.
- `krea_action_context.py` owns shared action-family predicates, axis context
  text, climax detection, and detail-density normalization used by action and
  POV formatter routes.
- `hardcore_action_metadata.py` owns shared action-family constants,
  normalization, and inference used by the builder and Krea formatter route.
- `pov_policy.py` owns shared POV labels, label filtering, source role-graph
  viewer replacement, and composition cleanup; `krea_pov.py` owns Krea-specific
  POV camera support text while delegating shared POV policy.
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
  density limiting for Krea action prose.
- `krea_action_positions.py` owns non-POV pose anchors, body-arrangement text,
  rear-entry detection, and action-position phrasing.
- `krea_action_details.py` owns non-climax item/detail cleanup for foreplay,
  outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths.
- `krea_action_climax.py` owns climax-specific role/detail cleanup and aftermath
  view dedupe.
- `krea_action_dispatch.py` owns non-POV role normalization, action-family
  classification, and family-specific detail cleanup.
- `krea_actions.py` owns final non-POV hardcore action sentence assembly.
- `krea_pov_actions.py` owns POV hardcore action sentence rewriting,
  first-person body geometry, and selected-position-axis priority before loose
  context fallback.
- `formatter_input.py` owns shared metadata/source JSON detection, trigger
  stripping, the shared prompt field-label inventory, prompt-field extraction,
  `Avoid:` splitting, and row-value fallback for Krea, SDXL, and caption
  routes.
- `route_metadata.py` owns shared row-level action-family, position-family,
  position-key, and formatter-hint reads so formatter routes do not normalize
  these fields independently.

Improve later:

- keep adding route-level smoke fixtures when new metadata fields start
  influencing formatter output;

### SDXL Formatter Path

Owner: `sdxl_formatter.py`.

Keep here:

- trigger behavior;
- style and quality presets;
- final style/body/quality prompt assembly;
- nude-weight setting;
- negative-prompt assembly.

Already isolated:

- `sdxl_format_route.py` owns top-level SDXL dispatch, including formatter
  profile application, shared target normalization via `formatter_target.py`,
  nude-weight normalization, metadata-vs-text input selection, single-vs-pair
  branching, final prompt/negative output shape, and fallback routing;
  `sdxl_formatter.py` keeps the public wrapper.
- `sdxl_tag_routes.py` owns normal metadata row tags and Insta/OF pair soft/hard
  tag extraction behind `SDXLRowTagRequest`, `SDXLPairTagRequest`,
  `SDXLTagRouteDependencies`, and `SDXLTagRoute`; `sdxl_formatter.py` keeps
  compatibility wrappers plus final style/quality/trigger assembly.
- `sdxl_tag_policy.py` owns SDXL tag splitting, tag-key dedupe, count inference,
  character descriptor tags, metadata-family hint tags, camera tags,
  explicit/nude helper tags, and route dependency assembly.
- metadata-family tag hint data from `action_family`, `position_family`, and
  `position_keys` stays in `sdxl_presets.py` and is read by `sdxl_tag_policy.py`.
- shared row route metadata reads from `route_metadata.py`.
- shared formatter input parsing from `formatter_input.py`.
- style presets, quality presets, default negative prompt, and action/position
  family tag hints from `sdxl_presets.py`.
- formatter profiles for manual controls, Pony flat-vector, SDXL photo, and
  plain flat-vector styles live in `sdxl_presets.py` and are exposed by
  `SxCP SDXL Formatter`.
- fallback field-label cleanup delegates to `formatter_input.py`.

Improve later:
- add route-level fixtures for any new SDXL model profile that needs different
  tag ordering.

### Naturalizer Path

Owner: `caption_naturalizer.py`.

Keep here:

- top-level natural caption orchestration;
- training-caption trigger behavior;
- style-tail policy from `caption_policy.py`.

Already isolated:

- `caption_format_route.py` owns top-level caption dispatch, including input
  hint normalization, shared target normalization via `formatter_target.py`,
  caption profile application, metadata-vs-text branching, trigger wrapping,
  final prose hygiene, and method/output shape;
  `caption_naturalizer.py` keeps the public wrapper.
- `caption_metadata_routes.py` owns metadata row natural-language assembly for
  single, couple, configured-cast, group/layout, and Insta/OF pair routes behind
  `CaptionMetadataRouteRequest`, `CaptionMetadataRouteDependencies`, and
  `CaptionMetadataRoute`; `caption_naturalizer.py` keeps compatibility wrappers,
  profile handling, trigger behavior, and text fallback.
- `caption_text_policy.py` owns caption sentence helpers, trigger wrapping,
  formatter-hint append, row-value fallback wrappers, cast text wrappers,
  single-caption front parsing, route dependency assembly, and caption metadata
  helper callbacks used by `caption_metadata_routes.py`.
- metadata-family action labels from `action_family` and `position_family` via
  `caption_policy.py`.
- shared row route metadata reads from `route_metadata.py`.
- shared formatter input parsing from `formatter_input.py`.
- shared cast descriptor parsing and label replacement from `krea_cast.py`.
- caption detail-level/style-policy normalization, clothing cleanup, and
  composition cleanup from `caption_policy.py`.
- caption profiles for manual controls, concise training captions, dense
  training captions, and browsing captions live in `caption_policy.py` and are
  exposed by `SxCP Caption Naturalizer`.

Improve later:
- add more caption profiles if a new training or browsing workflow needs a
  distinct default.

### Category JSON Path

Owner: `categories/*.json`.

Keep here:

- scalable prompt pool content;
- named scene/expression/composition pools;
- item templates and axes;
- direct category-specific wording.
- optional object-style item templates with route metadata such as
  `action_family`, `action_type`, `position_family`, `family`, `position_key`,
  `position_keys`, and `formatter_hint`; string templates remain valid and fall
  back to Python inference. Normalized formatter hints are routed into Krea,
  SDXL, and caption naturalization through `all` plus the matching formatter
  route only.

Improve later:

- keep `tools/prompt_map_audit.py` passing; it now checks referenced
  expression/composition/scene pools, item-template axes, object-template
  metadata values for both string and object templates, category/subcategory
  identity uniqueness, registered formatter policy coverage for route
  families, and critical route documentation plus expected smoke coverage.

### Node / UI Path

Owner: `__init__.py`, `node_builder.py`, `node_seed_resolution.py`,
`node_camera.py`, `node_character.py`, `node_hardcore_position.py`,
`node_formatter.py`, `node_insta.py`, `node_route_config.py`,
`node_profile_filter.py`, `loop_nodes.py`, `web/*.js`.

Keep here:

- ComfyUI node input/output declarations;
- widget behavior;
- button actions;
- dynamic input slots.
- direct and config-driven builder node declarations in `node_builder.py`.
- seed and resolution utility node declarations in `node_seed_resolution.py`.
- camera utility node declarations in `node_camera.py`.
- character pool, slot, and profile node declarations in `node_character.py`.
- hardcore position pool/filter node declarations in
  `node_hardcore_position.py`.
- caption/Krea2/SDXL formatter node declarations in `node_formatter.py`.
- Insta/OF options and prompt-pair node declarations in `node_insta.py`.
- route/category/location/composition/cast config node declarations in
  `node_route_config.py`.
- profile/filter/ethnicity-list node declarations in `node_profile_filter.py`.

Already isolated:

- direct and config-driven prompt builder nodes live in `node_builder.py`, with
  registration maps imported by `__init__.py`.
- seed axis salts/aliases, seed mode choices, lock builders, seed config
  parsing, row seed math, and deterministic axis RNG live in `seed_config.py`;
  seed/global-seed/seed-locker nodes live in `node_seed_resolution.py`, with
  registration maps imported by `__init__.py`.
- SDXL/Krea2 resolution utility nodes live in `node_seed_resolution.py`, with
  registration maps imported by `__init__.py`.
- camera/orbit/Qwen translator utility nodes live in `node_camera.py`, using
  `camera_config.py` for option lists and JSON builders, with registration maps
  imported by `__init__.py`.
- hair, age/body/eyes/clothing pools, manual character details, character
  slots, and profile save/load nodes live in `node_character.py`, with
  registration maps imported by `__init__.py`.
- hardcore position pool and action filter nodes live in
  `node_hardcore_position.py`, with registration maps imported by
  `__init__.py`.
- caption naturalizer, Krea2 formatter, and SDXL formatter nodes live in
  `node_formatter.py`, with registration maps imported by `__init__.py`.
- Insta/OF options and dual prompt-pair nodes live in `node_insta.py`, with
  registration maps imported by `__init__.py`.
- category preset, location/composition pool, location theme, and cast config
  utility nodes live in `node_route_config.py`, with registration maps imported
  by `__init__.py`.
- generation profile, advanced filter, and ethnicity list utility nodes live in
  `node_profile_filter.py`, with registration maps imported by `__init__.py`.
- index-switch constants, index-base normalization, missing-input behavior,
  route-output selection, status text, and lazy-input selection live in
  `index_switch_policy.py`; `loop_nodes.py` keeps the ComfyUI node wrapper and
  accumulator/loop runtime logic.
- node input tooltip inventory, node-specific tooltip overrides, dynamic input
  fallback tooltip rules, and tooltip injection live in `node_tooltips.py`;
  `__init__.py` only applies the installer to the assembled node registry.
- node registration drift is checked by `tools/prompt_map_audit.py`: concrete
  `SxCP...` node classes in node modules must be present in their module class
  mappings and matching display-name mappings before they can silently
  disappear from ComfyUI.
- profile-save and accumulator server payload handling lives in
  `server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
  JSON responses, and `tools/prompt_smoke.py` covers the handlers without
  importing ComfyUI.

Improve later:

- split remaining large node classes into files by family;
- keep node display names, return names, and docs in sync through the audit
  helper;
- add more endpoint tests when new server routes are introduced.

## Path-Specific Improvements

### Prompt Builder

Near-term:

- Add final row hygiene already done through `prompt_hygiene.py`.
- Add a metadata smoke checker for representative generated rows and static
  formatter fixtures through `tools/prompt_smoke.py`.
- Normalize every row with one function before JSON serialization.

Medium-term:

- Keep category loading, prompt-row routing, and role-graph routing in their
  extracted owner modules instead of rebuilding them inside
  `prompt_builder.py`.
- Add new template metadata only when a generated route needs a concrete action,
  position, or formatter hint that is not already expressible.

### Insta/OF Pair

Near-term:

- Normalize pair metadata with one helper, including embedded row prompt,
  caption, negative, and side-specific metadata synchronization.
- Confirm pair prompts, captions, and soft/hard rows carry the same sanitized
  scene/camera/clothing fields.
- 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 and person, scene, content,
  pose, expression, and composition rerolls.

Medium-term:

- Keep camera and clothing phases in `pair_camera.py` and `pair_clothing.py`;
  extend those modules when a pair output shows concrete camera/clothing drift.
- Add pair smoke fixtures only when a new pair option or observed output changes
  soft/hard cast, POV, explicit-nude, or split-camera metadata behavior.

### Krea2

Near-term:

- Add final prose hygiene already done through `prompt_hygiene.py`.
- Add smoke coverage through `tools/prompt_smoke.py` for metadata-driven Krea2
  formatting across built-in rows, hardcore rows, same-cast pairs, and POV
  pairs.
- Cover camera-scene preservation through `tools/prompt_smoke.py` for single
  rows, split soft/hard pair cameras, and POV camera-scene routing.
- Cover config-node routing through `tools/prompt_smoke.py` for category, cast,
  generation profile, seed lock, camera, location theme, and composition config.
- Cover close foreplay and POV penetration Krea routes so raw labels, invalid
  surface grammar, normal third-person camera text, and composition punctuation
  drift are caught.
- Cover POV outercourse, oral, penetration, anal, and front/back double-contact
  Krea routes so selected position geometry stays synchronized with metadata.
- Cover generated climax routes through Krea, SDXL, and natural caption outputs
  so source aftermath placement and formatter details cannot drift apart.
- Cover generated interaction routes through Krea, SDXL, and natural caption
  outputs so source contact/guidance/presentation wording stays metadata-driven.
- Cover generated fallback role routes through Krea, SDXL, and natural caption
  outputs so solo and same-sex paths do not remain untested edge behavior.
- Keep route simulation coverage updated when adding action or position
  families, so generated Krea2, SDXL, and natural-caption paths prove the new
  family reaches formatter metadata routes.

Medium-term:

- Keep action-family rewriting dispatched through `krea_action_dispatch.py` and
  the existing `krea_action_*` / `krea_pov*` helpers.
- Add or split Krea helpers only for an observed route failure or a new
  action-family metadata path.

### SDXL

Near-term:

- Add final tag hygiene already done through `prompt_hygiene.py`.
- Add smoke tests for trigger preservation and duplicate tag removal through
  `tools/prompt_smoke.py`.

Medium-term:

- Make style/quality presets data-driven.

### Naturalizer

Near-term:

- Add final prose hygiene already done through `prompt_hygiene.py`.
- Verify training captions keep trigger exactly once through
  `tools/prompt_smoke.py`.

Medium-term:

- Add caption profiles for training and browsing use cases.

### Camera / Scene

Near-term:

- Keep Qwen/orbit as camera source.
- Keep scene-camera adapters scoped by location family.
- Use the memory note in
  `/home/ethanfel/.codex/memories/scene-camera-system.md` when editing POV.
- Keep `scene_camera_adapters.py` as the owner for location-aware camera prose;
  add new location families there one at a time.
- Keep `row_camera.py` as the owner for inserting camera/scene directives into
  generated rows, including POV suppression of normal third-person camera text.

Medium-term:

- Build new adapters one location family at a time.

## Invariants To Preserve

- Metadata is the preferred formatter input.
- Prompt Builder should output structured rows even if raw prompt text is rough.
- Krea should fix prose and semantic action readability, not category selection.
- SDXL should produce tag-style output and preserve model triggers as requested.
- Naturalizer should output training-friendly captions without changing the
  selected content.
- Generic cleanup belongs in `prompt_hygiene.py`; semantic cleanup belongs in
  the owning route.

## Recommended Next Passes

1. Keep new node classes in their owning `node_*.py` or `loop_nodes.py`
   module, with registration/display docs covered by the audit.
2. Keep `hardcore_role_graphs.py` as the dispatch surface; add behavior in the
   existing `hardcore_role_*` action-family modules only when a concrete
   generated edge case needs it.
3. Add route-level smoke fixtures only for observed generated edge cases or new
   metadata fields that affect Krea2, SDXL, or caption output.
