# State & artifacts — the on-disk SSOT layer: where every piece of pipeline
# state physically lives and who reads/writes it.
#   - output_layout + project_path_construction: ProjectPaths is the v3 path
#     SSOT (assets/{cls}/{subject}/base/pool/{kind}/ + base/sheets/, prep/ep_NNN/,
#     renders/ep_NNN/). Root = $RECOIL_PROJECTS_ROOT (CLAUDE_DATA), NOT in-repo.
#   - breakdown: breakdown_extract writes the per-episode mention_ledger.json
#     (scene mentions: location/sublocation/prop/prop_state/wardrobe_change).
#   - gen_sublocations: SOLE writer of base/location.json (sublocation registry).
#   - ref_promotion: KNOWN-BROKEN — THREE incompatible promotion mechanisms,
#     none of which write where the production resolver reads (the divergence).
#   - ssot_manifest.yaml is itself the capability SSOT every topology node FKs into.
# Capability state/canonical/deprecated live in ssot_manifest.yaml, not copied here.
subsystem: state_artifacts

capabilities:
  - id: output_layout
    reduced_tier: always
    risk_level: high
  - id: project_path_construction
    reduced_tier: always
    risk_level: high
  - id: ref_promotion
    reduced_tier: always
    risk_level: critical

entrypoints:
  - id: breakdown_extract_cli
    file: recoil/pipeline/tools/breakdown_extract_cli.py
    symbol: main
    invoke: "python3 recoil/pipeline/tools/breakdown_extract_cli.py --project <slug> --episode <N> [--dry-run]"
    kind: cli
    summary: "Extract/refresh the per-episode mention ledger via extract_mention_ledger; carries forward unchanged scenes by scene_hash."
    produces: [mention_ledger]
    risk_level: medium

  - id: gen_sublocations_cli
    file: recoil/pipeline/tools/gen_sublocations.py
    symbol: main
    invoke: "python3 recoil/pipeline/tools/gen_sublocations.py --project <slug> --location <id> [--probe --dry-run --adjacency-confirm --restamp]"
    kind: cli
    summary: "SOLE writer of base/location.json sublocation registry (post-2026-06-11 retirement of other writers); generates derived sublocation refs and reconciles source_sha256 to bible description hashes."
    produces: [location_registry]
    risk_level: medium

  - id: prep_character_angles_cli
    file: recoil/pipeline/tools/prep_character_angles.py
    symbol: prep_character
    invoke: "python3 recoil/pipeline/tools/prep_character_angles.py  # Path A: rotate an intake/MJ hero into angle refs"
    kind: cli
    summary: "ref_promotion mechanism #1 — process_intake copies a hero to assets/char/<slug>/hero.<ext> (subject top-level, NOT base/pool); then generates angles. Non-conformant: resolver does not read this path."
    capabilities: [ref_promotion]
    risk_level: high

artifacts:
  - id: ssot_manifest
    path: recoil/architecture/ssot_manifest.yaml
    written_by: [human, spec_register_capability]
    read_by: [build_topology, all_topology_nodes]
    schema: "capability map: {capability_id: {canonical, state, deprecated_paths, ...}}"
    reduced_tier: always
    risk_level: critical

  - id: mention_ledger
    path: "{project}/prep/ep_NNN/breakdown/mention_ledger.json"
    written_by: [breakdown_extract_cli]
    read_by: [breakdown_gate_cli, coverage_validator]
    schema: "scenes[].mentions[] with kind in {location, sublocation, prop, prop_state, wardrobe_change}; common required: kind, surface_text, span_quote, scene_id, scene_hash. Gate-A cache, NOT SSOT."
    risk_level: high

  - id: location_registry
    path: "{project}/assets/loc/{location_id}/base/location.json"
    written_by: [gen_sublocations_cli]
    read_by: [sublocation_registry, ref_resolution]
    schema: "sublocations{name: {ref, source_sha256}}; absence = undecomposed = everything permitted (fail-OPEN); empty-but-present = decomposed-with-nothing-allowed."
    risk_level: high

  - id: asset_pool_refs
    path: "{project}/assets/{cls}/{subject}/base/pool/{kind}/"
    written_by: []
    read_by: [project_path_construction, ref_resolution]
    schema: "v3 canonical ref pool, kind in {identity, turn, expr, loc, prop, scene}; resolve_ref reads HERE."
    reduced_tier: always
    risk_level: critical

  - id: composite_sheets
    path: "per-kind via ProjectPaths.sheet_path — char {project}/assets/char/<subject>/base/sheets/, loc {project}/assets/loc/<subject>/sheets/, prop {project}/assets/prop/<subject>/ (loc/prop do NOT use base/sheets/)"
    written_by: [generate_composite_sheet._sheet_dest]
    read_by: [ref_resolution]
    schema: "v3 composite multi-angle sheets (the Seedance/Kling lock). The per-kind layout is intentional and has ONE home: ProjectPaths.sheet_path (REC-213 C1+C2) — reader (resolve_sheet_asset) and writer (_sheet_dest) both call it."
    risk_level: high

  - id: promoted_hero_top_level
    path: "{project}/assets/{cls}/{subject}/{role|hero}.{ext}"
    written_by: [prep_character_angles_cli, set_hero_junction, promote_grid_junction]
    read_by: []
    schema: "subject TOP-LEVEL promotion target (NOT base/pool/). The production resolver_ref does NOT read here → promoted heroes are orphaned. This is the ref_promotion KNOWN-BROKEN root."
    reduced_tier: always
    risk_level: critical

symbols:
  - id: project_paths
    file: recoil/core/paths.py
    symbol: ProjectPaths
    role: "v3 project-scoped path SSOT. Root via projects_root() → RECOIL_PROJECTS_ROOT env (CLAUDE_DATA) then pipeline_config.json, else raises. asset_look_dir/pool_dir/episode_breakdown_dir/get_location_sheets_dir all hang off here; asset_kind_dir + sequences_dir raise DeprecatedPathAPIError. THE construction junction every artifact path traverses."
    reduced_tier: always
    risk_level: high

  - id: resolve_ref_reader
    file: recoil/core/paths.py
    symbol: ProjectPaths.resolve_ref
    role: "The path-layer ref reader: scans asset_look_dir(base)/ for candidate stems, NOT base/pool/. A second resolver alongside core/ref_resolver.py — the version-split this whole topology forces (ssot_manifest ref_resolution: transitioning). Reads a DIFFERENT location than every promotion writer."
    risk_level: critical

  - id: set_hero_junction
    file: recoil/pipeline/_lib/asset_ops.py
    symbol: set_hero
    role: "ref_promotion mechanism #2 — copies source → asset_subject_dir/<phase>_hero.<ext> (subject top-level) with a sidecar manifest + op log. Different target than promote_grid and prep_character_angles."
    risk_level: high

  - id: promote_grid_junction
    file: recoil/pipeline/_lib/ref_image_ops.py
    symbol: promote_grid
    role: "ref_promotion mechanism #3 — splits a grid, writes BOTH asset_subject_dir/<role>.png (canonical) AND <slug>_<role>.png (legacy) at subject top-level. Yet another non-pool target; resolver prefers the canonical-named top-level file but production reads base/pool."
    risk_level: high

routes:
  # ref_promotion: writers land at the subject top-level; the production reader reads base/pool — they diverge.
  - id: promotion_writer_route
    capability: ref_promotion
    surface: promotion
    entry: recoil/pipeline/_lib/asset_ops.py::set_hero
    resolves_via: recoil/pipeline/_lib/asset_ops.py::set_hero
    ref_kind: top_level_subject_dir   # set_hero / promote_grid / prep_character_angles write assets/{cls}/{subject}/
    gated_by: null
    risk_level: critical
  - id: production_reader_route
    capability: ref_promotion
    surface: production
    entry: recoil/core/ref_resolver.py::resolve_entity_refs
    resolves_via: recoil/core/ref_resolver.py::resolve_entity_refs
    ref_kind: base_pool_dir           # reads assets/{cls}/{subject}/base/pool/{kind}/ — NOT where the writers land
    gated_by: null
    risk_level: critical
  # location sheet path layout: char sheets carry base/, loc sheets do not.
  - id: char_sheet_path_route
    capability: project_path_construction
    surface: char
    entry: recoil/core/paths.py::get_character_sheets_dir
    resolves_via: recoil/core/paths.py::get_character_sheets_dir
    ref_kind: char_base_sheets        # assets/char/<id>/base/sheets/
    gated_by: null
    risk_level: medium
  - id: loc_sheet_path_route
    capability: project_path_construction
    surface: loc
    entry: recoil/core/paths.py::get_location_sheets_dir
    resolves_via: recoil/core/paths.py::get_location_sheets_dir
    ref_kind: loc_sheets              # assets/loc/<id>/sheets/ — no base/; diverges from char + from the loc registry
    gated_by: null
    risk_level: medium

divergences:
  - id: ref_promotion_three_writers
    capability: ref_promotion
    forks: [promotion_writer_route, production_reader_route]
    on_flag: null
    invariant: "ref_promotion MUST land refs where the production resolver reads. Today the three promotion mechanisms (set_hero, promote_grid, prep_character_angles.process_intake) ALL write to assets/{cls}/{subject}/ top-level, while resolve_entity_refs (the production payload resolver) reads assets/{cls}/{subject}/base/pool/{kind}/ — so no promotion output reaches production. Consolidation = one promotion writer + one resolver onto base/pool via a manifest pointer (REC-76 follow-up)."
    risk: "ssot_manifest marks ref_promotion canonical:null KNOWN-BROKEN. THREE incompatible mechanisms (asset_ops.set_hero copies, ref_image_ops.promote_grid copies w/ dual names, prep_character_angles renames) all promote toward subject top-level with non-conformant names; production resolver does not read the pool nor the top level it expects → promoted refs are silently orphaned. An operator promoting a hero sees success but production never picks it up."
    reduced_tier: always
    risk_level: critical

  - id: location_sheets_path_split
    capability: project_path_construction
    forks: [char_sheet_path_route, loc_sheet_path_route]
    on_flag: null
    invariant: "the per-kind sheet layout (char assets/char/<n>/base/sheets/, loc assets/loc/<n>/sheets/ — loc intentionally drops base/) has exactly ONE home: ProjectPaths.sheet_path (REC-213 C1+C2). Reader (resolve_sheet_asset) and writer (_sheet_dest) both go through it, so the per-kind branch cannot drift across code paths."
    risk: "RESOLVED by C1+C2: the per-kind difference is intentional and SSOT'd in ProjectPaths.sheet_path. Note loc composite sheets (sheets/) still live under a different parent than the loc registry (base/location.json) — a layout fact, no longer a code-path split."
    reduced_tier: always
    risk_level: low
