kirby-api 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. {kirby_api-0.2.2/kirby_api.egg-info → kirby_api-0.2.3}/PKG-INFO +2 -2
  2. kirby_api-0.2.3/kirby/_migrations/versions/20261005_0002_build_doc_fields.py +43 -0
  3. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/build_doc_store.py +30 -5
  4. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/cli.py +0 -49
  5. kirby_api-0.2.3/kirby/builds/json_importer.py +117 -0
  6. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/models.py +9 -0
  7. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/power_library_importer.py +69 -98
  8. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/routes.py +6 -6
  9. {kirby_api-0.2.2 → kirby_api-0.2.3/kirby_api.egg-info}/PKG-INFO +2 -2
  10. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby_api.egg-info/SOURCES.txt +1 -1
  11. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby_api.egg-info/requires.txt +1 -1
  12. {kirby_api-0.2.2 → kirby_api-0.2.3}/pyproject.toml +2 -2
  13. kirby_api-0.2.2/kirby/builds/json_exporter.py +0 -277
  14. kirby_api-0.2.2/kirby/builds/json_importer.py +0 -553
  15. {kirby_api-0.2.2 → kirby_api-0.2.3}/LICENSE +0 -0
  16. {kirby_api-0.2.2 → kirby_api-0.2.3}/README.md +0 -0
  17. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/__init__.py +0 -0
  18. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/README +0 -0
  19. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/alembic.ini +0 -0
  20. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/env.py +0 -0
  21. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/script.py.mako +0 -0
  22. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/1b7affd8668e_initial_schema_users_tenants_characters_.py +0 -0
  23. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260424_0001_combat_tables.py +0 -0
  24. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260429_0001_add_combatant_jsonb.py +0 -0
  25. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260501_0001_codex_targeted_pages.py +0 -0
  26. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260501_0002_hd_relational_schema.py +0 -0
  27. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260501_0003_definitions_codex_chunk_id.py +0 -0
  28. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260503_0001_per_template_base_values.py +0 -0
  29. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0001_pr1_relational_combat_schema.py +0 -0
  30. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0002_seed_tactics_and_profiles.py +0 -0
  31. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0003_backfill_combat_relational.py +0 -0
  32. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0004_drop_combat_jsonb_columns.py +0 -0
  33. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0005_pr1_index_audit_fills.py +0 -0
  34. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0006_pending_phase_modifiers.py +0 -0
  35. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0007_combat_session_scene_id.py +0 -0
  36. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0008_morale_expires_at_seq.py +0 -0
  37. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0009_is_stunned.py +0 -0
  38. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0010_tactic_catalog_expansion.py +0 -0
  39. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0011_slam_velocity_tactic.py +0 -0
  40. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0012_mind_control_state.py +0 -0
  41. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0013_mental_illusion_state.py +0 -0
  42. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0014_mental_entangle_state.py +0 -0
  43. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0015_was_surprised.py +0 -0
  44. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0016_catalog_expansion_for_new_actions.py +0 -0
  45. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0017_combat_started.py +0 -0
  46. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0018_physical_entangle.py +0 -0
  47. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0019_stat_drains.py +0 -0
  48. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0020_continuous_effects.py +0 -0
  49. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0021_lockout_state.py +0 -0
  50. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260504_0022_dispelled_powers.py +0 -0
  51. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260506_0001_campaign_management_v1.py +0 -0
  52. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260506_0002_extend_characters_for_pc.py +0 -0
  53. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260506_0003_combatant_control_mode.py +0 -0
  54. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260506_0004_brain_embeddings.py +0 -0
  55. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260507_0001_knowledge_tier.py +0 -0
  56. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260507_0002_normalize_jsonb.py +0 -0
  57. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260507_0003_drop_legacy_jsonb.py +0 -0
  58. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260507_0004_canon_embeddings.py +0 -0
  59. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260508_0001_comic_canon_init.py +0 -0
  60. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260511_0001_power_library_init.py +0 -0
  61. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260514_0001_combat_session_ai_provider.py +0 -0
  62. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260528_0001_coordinate_raw_tables.py +0 -0
  63. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260528_0002_coordination_initiator.py +0 -0
  64. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0001_character_def_rename.py +0 -0
  65. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0002_character_instance.py +0 -0
  66. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0003_team_def.py +0 -0
  67. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0004_team_member_def.py +0 -0
  68. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0005_team_instance.py +0 -0
  69. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0006_team_def_real_history.py +0 -0
  70. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0007_team_page.py +0 -0
  71. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0008_team_narrative.py +0 -0
  72. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260529_0009_migrate_org_journal_entries_to_team.py +0 -0
  73. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260530_0002_templates_catalog_columns.py +0 -0
  74. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260530_0003_reconcile_template_category_vocab.py +0 -0
  75. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260531_0001_align_catalog_to_engine_schema.py +0 -0
  76. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260601_0001_build_doc_fields.py +0 -0
  77. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260605_0001_proposals.py +0 -0
  78. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260605_0002_membership_roster.py +0 -0
  79. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260605_0003_xp_award.py +0 -0
  80. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260606_0001_base_drift_guard.py +0 -0
  81. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260607_0001_maneuver_details.py +0 -0
  82. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260609_0001_scene_wall_def_value.py +0 -0
  83. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260609_0002_combat_session_constructs.py +0 -0
  84. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260611_0001_arena_hazard_kind_status_tokens.py +0 -0
  85. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260611_0002_tactic_entry_role.py +0 -0
  86. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260611_0003_seed_archetype_profiles.py +0 -0
  87. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260612_0001_combatant_held_construct.py +0 -0
  88. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260613_0001_combatant_is_hidden.py +0 -0
  89. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260613_0002_combatant_flashed_sense.py +0 -0
  90. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260614_0001_construct_sense_group.py +0 -0
  91. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260614_0002_image_decoy.py +0 -0
  92. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260618_0001_character_extraction_tables.py +0 -0
  93. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260621_0001_combat_session_combatant_slot.py +0 -0
  94. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260621_0002_vpp_power.py +0 -0
  95. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260623_0001_power_categories.py +0 -0
  96. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260726_0001_combat_intent.py +0 -0
  97. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260727_0001_intent_checks_since_form.py +0 -0
  98. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260728_0001_stunned_at_sequence.py +0 -0
  99. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260729_0001_intent_budget_turns.py +0 -0
  100. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260730_0001_standable_structures.py +0 -0
  101. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260731_0001_climb_difficulty.py +0 -0
  102. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260731_0002_widen_character_object_alias.py +0 -0
  103. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260802_0001_identity_by_id_not_xmlid.py +0 -0
  104. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260803_0001_combatant_opening_state.py +0 -0
  105. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260804_0001_combat_random_fight.py +0 -0
  106. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260804_0002_presence_attack_effects.py +0 -0
  107. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260816_0001_template_level_power.py +0 -0
  108. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260831_0001_construct_no_teleport.py +0 -0
  109. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260831_0002_physical_entangle_state.py +0 -0
  110. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260831_0003_character_object_text.py +0 -0
  111. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260902_0001_combatant_in_hero_id.py +0 -0
  112. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260902_0002_object_affects_flags.py +0 -0
  113. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260917_0001_session_names_a_seat.py +0 -0
  114. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260918_0000_drop_combat_tables.py +0 -0
  115. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260918_0001_harness_tables.py +0 -0
  116. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20260918_0002_narration_speaker_id.py +0 -0
  117. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/20261005_0001_knowledge_indexes.py +0 -0
  118. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/21980c836860_add_asset_id_refs.py +0 -0
  119. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/_migrations/versions/ba28e71c955d_codex_init.py +0 -0
  120. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/admin/__init__.py +0 -0
  121. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/__init__.py +0 -0
  122. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/authz.py +0 -0
  123. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/dependencies.py +0 -0
  124. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/jwt.py +0 -0
  125. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/models.py +0 -0
  126. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/routes.py +0 -0
  127. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/auth/users.py +0 -0
  128. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/__init__.py +0 -0
  129. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/category_extraction.py +0 -0
  130. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/codex_linker.py +0 -0
  131. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/combatant_loader.py +0 -0
  132. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/importer.py +0 -0
  133. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/power_categories.py +0 -0
  134. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/power_library_service.py +0 -0
  135. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/recompute.py +0 -0
  136. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/routes.py +0 -0
  137. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/builds/template_provider.py +0 -0
  138. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/__init__.py +0 -0
  139. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/_shared.py +0 -0
  140. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/membership_service.py +0 -0
  141. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/models.py +0 -0
  142. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes.py +0 -0
  143. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_acl.py +0 -0
  144. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_brain.py +0 -0
  145. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_documents.py +0 -0
  146. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_knowledge.py +0 -0
  147. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_membership.py +0 -0
  148. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_relationships.py +0 -0
  149. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_roster.py +0 -0
  150. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_sessions_events.py +0 -0
  151. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/routes_xp.py +0 -0
  152. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/__init__.py +0 -0
  153. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/brain.py +0 -0
  154. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/conflicts.py +0 -0
  155. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/embeddings.py +0 -0
  156. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/extract.py +0 -0
  157. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/generation.py +0 -0
  158. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/legacy_translation.py +0 -0
  159. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/permissions.py +0 -0
  160. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/campaigns/services/recap.py +0 -0
  161. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/__init__.py +0 -0
  162. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/_shared.py +0 -0
  163. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/build_routes.py +0 -0
  164. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/combat_seed.py +0 -0
  165. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/corpus_copy.py +0 -0
  166. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/models.py +0 -0
  167. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/proposals/__init__.py +0 -0
  168. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/proposals/routes.py +0 -0
  169. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/proposals/service.py +0 -0
  170. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/services/__init__.py +0 -0
  171. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/services/character_info_backfill.py +0 -0
  172. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/services/effective.py +0 -0
  173. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/services/writeup_persistence.py +0 -0
  174. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/variants_service.py +0 -0
  175. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/characters/xp_service.py +0 -0
  176. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/cli.py +0 -0
  177. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/__init__.py +0 -0
  178. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/cli.py +0 -0
  179. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/deps.py +0 -0
  180. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/models.py +0 -0
  181. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/repositories.py +0 -0
  182. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/routes.py +0 -0
  183. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/schemas.py +0 -0
  184. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/__init__.py +0 -0
  185. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/embed.py +0 -0
  186. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/extract.py +0 -0
  187. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/index.py +0 -0
  188. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/orchestrator.py +0 -0
  189. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/search.py +0 -0
  190. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/villain_extractor.py +0 -0
  191. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/services/villain_writeup_linker.py +0 -0
  192. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/codex/workers.py +0 -0
  193. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/__init__.py +0 -0
  194. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/choosers.py +0 -0
  195. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/create.py +0 -0
  196. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/events.py +0 -0
  197. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/harness.py +0 -0
  198. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/models.py +0 -0
  199. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/__init__.py +0 -0
  200. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/models.py +0 -0
  201. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/outcome.py +0 -0
  202. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/roster.py +0 -0
  203. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/routes.py +0 -0
  204. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/runner.py +0 -0
  205. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/schemas.py +0 -0
  206. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/session.py +0 -0
  207. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/random_fight/worker.py +0 -0
  208. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/routes.py +0 -0
  209. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/schemas.py +0 -0
  210. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/services/__init__.py +0 -0
  211. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/services/connection_hub.py +0 -0
  212. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/services/publish.py +0 -0
  213. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/combat/services/subscriber.py +0 -0
  214. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/comic_canon/__init__.py +0 -0
  215. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/comic_canon/models.py +0 -0
  216. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/comic_canon/routes.py +0 -0
  217. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/comic_canon/services/__init__.py +0 -0
  218. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/comic_canon/services/archetypes.py +0 -0
  219. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/comic_canon/services/embeddings.py +0 -0
  220. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/engines/__init__.py +0 -0
  221. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/engines/cost/__init__.py +0 -0
  222. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/engines/cost/hdc.py +0 -0
  223. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/library/__init__.py +0 -0
  224. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/main.py +0 -0
  225. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/media/__init__.py +0 -0
  226. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/media/asset_uses.py +0 -0
  227. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/media/client.py +0 -0
  228. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/media/routes.py +0 -0
  229. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/media/schemas.py +0 -0
  230. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/media/taxonomy.py +0 -0
  231. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/settings.py +0 -0
  232. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/shared/__init__.py +0 -0
  233. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/shared/db.py +0 -0
  234. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/teams/__init__.py +0 -0
  235. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/teams/models.py +0 -0
  236. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/teams/routes.py +0 -0
  237. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/teams/schemas.py +0 -0
  238. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/teams/services/__init__.py +0 -0
  239. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/teams/services/effective.py +0 -0
  240. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/templates/__init__.py +0 -0
  241. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/tenants/__init__.py +0 -0
  242. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/tenants/models.py +0 -0
  243. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/tenants/routes.py +0 -0
  244. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/users/__init__.py +0 -0
  245. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/users/routes.py +0 -0
  246. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby/version.py +0 -0
  247. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby_api.egg-info/dependency_links.txt +0 -0
  248. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby_api.egg-info/entry_points.txt +0 -0
  249. {kirby_api-0.2.2 → kirby_api-0.2.3}/kirby_api.egg-info/top_level.txt +0 -0
  250. {kirby_api-0.2.2 → kirby_api-0.2.3}/setup.cfg +0 -0
  251. {kirby_api-0.2.2 → kirby_api-0.2.3}/tests/test_knowledge_indexes_migration.py +0 -0
  252. {kirby_api-0.2.2 → kirby_api-0.2.3}/tests/test_public_vocabulary.py +0 -0
  253. {kirby_api-0.2.2 → kirby_api-0.2.3}/tests/test_teardown.py +0 -0
  254. {kirby_api-0.2.2 → kirby_api-0.2.3}/tests/test_wheel_contents.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kirby-api
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Kirby — HERO System VTT backend API (FastAPI mono)
5
5
  Author: PeterB
6
6
  License: PolyForm Noncommercial License 1.0.0
@@ -22,7 +22,7 @@ Requires-Dist: cryptography>=44
22
22
  Requires-Dist: python-jose[cryptography]>=3.3
23
23
  Requires-Dist: pyjwt[crypto]>=2.10
24
24
  Requires-Dist: redis>=5.2
25
- Requires-Dist: kirby-cost>=0.6.1
25
+ Requires-Dist: kirby-cost>=0.6.2
26
26
  Requires-Dist: lxml>=5.3
27
27
  Requires-Dist: python-multipart>=0.0.20
28
28
  Requires-Dist: email-validator>=2.2
@@ -0,0 +1,43 @@
1
+ """columns for the build-doc fields kirby-cost 0.6.2 writes
2
+
3
+ kirby-cost 0.6.2 stops dropping three things from the canonical build doc,
4
+ and a doc stored here must keep them or the database rebuild differs from the
5
+ engine's own load:
6
+
7
+ * ``native_tongue`` on a Language -> ``object_skill_mode.native_tongue``.
8
+ A native tongue is free; without the flag Bokor rebuilds at 279 against
9
+ his file's 276.
10
+ * the Resistant Protection split -> ``character_objects.pd_levels`` /
11
+ ``ed_levels`` / ``md_levels`` / ``powd_levels``. Cost is by LEVELS, so the
12
+ split moves no points; it is which half is physical, which a fight needs.
13
+ * the EQUIPMENT section needs no column: it is stored as rows of
14
+ object_type ``equipment``.
15
+
16
+ All nullable and additive: NULL reads back as "not written", which is what
17
+ every row stored before this migration is.
18
+
19
+ Revision ID: 20261005_0002
20
+ Revises: 20261005_0001
21
+ Create Date: 2026-10-05
22
+ """
23
+ import sqlalchemy as sa
24
+ from alembic import op
25
+
26
+ revision = "20261005_0002"
27
+ down_revision = "20261005_0001"
28
+ branch_labels = None
29
+ depends_on = None
30
+
31
+ _SPLIT = ("pd_levels", "ed_levels", "md_levels", "powd_levels")
32
+
33
+
34
+ def upgrade() -> None:
35
+ for col in _SPLIT:
36
+ op.add_column("character_objects", sa.Column(col, sa.Integer(), nullable=True))
37
+ op.add_column("object_skill_mode", sa.Column("native_tongue", sa.Boolean(), nullable=True))
38
+
39
+
40
+ def downgrade() -> None:
41
+ op.drop_column("object_skill_mode", "native_tongue")
42
+ for col in _SPLIT:
43
+ op.drop_column("character_objects", col)
@@ -34,7 +34,9 @@ _SCALAR_FIELDS = ("levels", "option_id", "input", "notes", "text", "base_cost",
34
34
  # therefore means "not stated", i.e. the engine's True.
35
35
  "affects_primary", "affects_total",
36
36
  "level_cost", "level_value", "min_cost", "max_cost",
37
- "selected", "required", "private", "group", "sense_active")
37
+ "selected", "required", "private", "group", "sense_active",
38
+ # Resistant Protection's split (kirby-cost >= 0.6.2).
39
+ "pd_levels", "ed_levels", "md_levels", "powd_levels")
38
40
  _COL = {"input": "input_value", # build-doc 'input' is stored as input_value
39
41
  "text": "text_value", # 'text' collides with the SQL type name
40
42
  "group": "sense_group"} # 'group' is a SQL reserved word -> sense_group
@@ -44,12 +46,15 @@ _COL = {"input": "input_value", # build-doc 'input' is stored as input_value
44
46
  # min_set/max_set flag is on (value may be exactly 0.0). The DB distinguishes
45
47
  # "set to 0.0" (stored 0.0) from "unset" (stored NULL), so a NULL check alone
46
48
  # is the right gate for those two.
47
- _ZERO_SUPPRESSED = {"levels", "base_cost", "level_cost", "level_value"}
49
+ _ZERO_SUPPRESSED = {"levels", "base_cost", "level_cost", "level_value",
50
+ "pd_levels", "ed_levels", "md_levels", "powd_levels"}
48
51
  _SECTIONS = ("characteristics", "powers", "skills", "perks", "talents", "disadvantages",
49
- "martial_arts")
52
+ "martial_arts", "equipment")
53
+ # Carried gear is its own section (kirby-cost >= 0.6.2) and its own row type,
54
+ # so it reconstructs into "equipment" rather than joining the innate powers.
50
55
  _OTYPE = {"characteristics": "characteristic", "powers": "power", "skills": "skill",
51
56
  "perks": "perk", "talents": "talent", "disadvantages": "complication",
52
- "martial_arts": "maneuver"}
57
+ "martial_arts": "maneuver", "equipment": "equipment"}
53
58
  _SECTION_TYPES = frozenset(_OTYPE.values())
54
59
 
55
60
  # ForceWall flat fields (engine emits length_levels/height_levels/body_levels)
@@ -109,7 +114,8 @@ def _persist_object(db, character_id, o, *, object_type, parent_id, position,
109
114
  # presence of 'familiarity' marks a skill-mode row.
110
115
  if "familiarity" in o:
111
116
  db.add(ObjectSkillMode(object_id=row.id,
112
- **{f: o.get(f) for f in _SKILL_FIELDS}))
117
+ **{f: o.get(f) for f in _SKILL_FIELDS},
118
+ native_tongue=o.get("native_tongue")))
113
119
  # Maneuver detail row (marker: engine emits "maneuver": True on Maneuver objects).
114
120
  # Note: o.get(f) is not None keeps empty-string effect ("") — do NOT change to truthiness.
115
121
  if o.get("maneuver"):
@@ -142,6 +148,21 @@ def _persist_object(db, character_id, o, *, object_type, parent_id, position,
142
148
  parent_id=row.id, position=0, costs=costs)
143
149
  return row
144
150
 
151
+ def persist_object(db: Session, character_id, o: dict[str, Any], *,
152
+ object_type: str, parent_id: int | None, position: int,
153
+ costs: dict[str, dict[str, float]] | None = None) -> CharacterObject:
154
+ """Write ONE build-doc object (and its modifiers, adders, sub-powers,
155
+ Endurance Reserve REC and typed child rows) under ``character_id``.
156
+
157
+ The same writer ``persist_build_doc`` uses for a whole character, for a
158
+ caller that stores objects one at a time — the power library catalogues
159
+ single powers under a shared owner. ``costs`` is keyed by doc id, as
160
+ ``extract_costs(...).per_object`` is.
161
+ """
162
+ return _persist_object(db, character_id, o, object_type=object_type,
163
+ parent_id=parent_id, position=position, costs=costs)
164
+
165
+
145
166
  def save_build_doc(db: Session, character, doc: dict[str, Any]) -> tuple[dict[str, Any], Any]:
146
167
  """Persist an edited build doc onto a character. Normalizes through the
147
168
  engine (build_from_json validates + canonicalizes), then persist_loaded
@@ -292,6 +313,10 @@ def _row_to_doc_obj(row, rows_by_id, children_by_parent, fw, sm, mv, prov, rowid
292
313
  o[f] = val
293
314
  else:
294
315
  o[f] = bool(val)
316
+ # Written by the engine on a Language only, so NULL means "not a
317
+ # Language" (or stored before the column existed) and stays absent.
318
+ if smr.native_tongue is not None:
319
+ o["native_tongue"] = bool(smr.native_tongue)
295
320
  if row.id in prov:
296
321
  o["provides"] = prov[row.id]
297
322
  # Maneuver detail fields (reverse of _persist_object; emission conditions
@@ -71,28 +71,6 @@ def cmd_import_template(args) -> int:
71
71
  return 0
72
72
 
73
73
 
74
- def cmd_import_corpus(args) -> int:
75
- from kirby.builds.json_importer import (
76
- JSONImporter, _resolve_owner, _resolve_tenant,
77
- )
78
-
79
- with SessionLocal() as db:
80
- tenant_id = _resolve_tenant(db, args.tenant)
81
- owner_id = _resolve_owner(db, args.owner)
82
- importer = JSONImporter(db, tenant_id=tenant_id, owner_id=owner_id)
83
- counts = importer.import_directory(
84
- args.corpus_dir,
85
- glob=args.glob,
86
- skip_prefix=tuple(args.skip_prefix),
87
- )
88
-
89
- sys.stderr.write("\nImported by source:\n")
90
- for src in sorted(counts):
91
- sys.stderr.write(f" {src:12s} {counts[src]:>5d}\n")
92
- sys.stderr.write(f" {'TOTAL':12s} {sum(counts.values()):>5d}\n")
93
- return 0
94
-
95
-
96
74
  def cmd_backfill_power_categories(args) -> int:
97
75
  """Label stored powers + VPP pools with special-effect categories.
98
76
 
@@ -612,33 +590,6 @@ def main() -> int:
612
590
  )
613
591
  p_import.set_defaults(func=cmd_import_template)
614
592
 
615
- p_corpus = sub.add_parser(
616
- "import-corpus",
617
- help="Bulk-import a JSON corpus directory into characters + character_objects",
618
- )
619
- p_corpus.add_argument(
620
- "corpus_dir",
621
- help="Directory containing JSON files (e.g. tests/fixtures/oracle/)",
622
- )
623
- p_corpus.add_argument(
624
- "--tenant", default="throwback",
625
- help="Tenant slug to attribute imported characters to (default: throwback)",
626
- )
627
- p_corpus.add_argument(
628
- "--owner", default="local-bootstrap",
629
- help="User sub to set as owner (default: local-bootstrap)",
630
- )
631
- p_corpus.add_argument(
632
- "--glob", default="*.json",
633
- help="Filename glob within corpus_dir (default: *.json)",
634
- )
635
- p_corpus.add_argument(
636
- "--skip-prefix", action="append", default=[],
637
- help="Skip files starting with this prefix (repeatable). "
638
- "E.g. --skip-prefix bestiary__ to do villains only.",
639
- )
640
- p_corpus.set_defaults(func=cmd_import_corpus)
641
-
642
593
  p_reimport = sub.add_parser(
643
594
  "reimport-hdc",
644
595
  help="Re-import a directory of .hdc files (idempotent; backfills maneuvers)",
@@ -0,0 +1,117 @@
1
+ """Character import — the canonical path only.
2
+
3
+ ``JSONImporter.import_hdc_file`` parses an .hdc through the canon engine and
4
+ persists the complete build doc relationally (``persist_loaded``): the engine
5
+ is the single source of structure AND cost.
6
+
7
+ The module used to also read HD-shaped JSON files (the old HDCConverter
8
+ corpus) and write rows by hand, summing ``total_cost`` itself and filling
9
+ child tables no reader used. That path is gone (kirby-api 0.2.3): the corpus
10
+ is the database, and an .hdc enters it once, through ``import_hdc_file``. The
11
+ class keeps its name because the tests and tooling that construct it do.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from pathlib import Path
17
+ from uuid import UUID
18
+
19
+ from sqlalchemy.orm import Session
20
+
21
+ from kirby.auth.models import User
22
+ from kirby.characters.models import CharacterDef
23
+ from kirby.tenants.models import Tenant
24
+
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class JSONImporter:
30
+ """Imports .hdc files through the canon engine into kirby's tables."""
31
+
32
+ def __init__(
33
+ self, db: Session, tenant_id: UUID, owner_id: UUID,
34
+ *, category_bearer: str = "",
35
+ ) -> None:
36
+ self.db = db
37
+ self.tenant_id = tenant_id
38
+ self.owner_id = owner_id
39
+ # GM Tannhauser JWT used to label powers with special-effect categories
40
+ # via the generation service. Optional: without it, import proceeds and pools stay
41
+ # unrestricted (see _populate_categories).
42
+ self.category_bearer = category_bearer
43
+
44
+ # ---------------------------------------------------------------- public
45
+
46
+ def import_hdc_file(
47
+ self, hdc_path: str | Path, *, source: str, is_npc: bool = True,
48
+ ) -> CharacterDef:
49
+ """Parse one .hdc via the canon engine, persist its complete build doc
50
+ relationally (Spec 2/3). The engine is the single source of structure AND
51
+ cost; kirby stores the build doc + a recomputable cost cache."""
52
+ from kirby.engines.cost.hdc import build_loader
53
+ from kirby.builds.build_doc_store import persist_loaded
54
+
55
+ hdc_path = Path(hdc_path)
56
+ # Costs come from kirby's own Main6E catalogue — the same source
57
+ # load_hdc uses. kirby-cost ships no template, so a bare HDCLoader()
58
+ # would depend on a file the deployment may not have.
59
+ loaded = build_loader(self.db).load_file(str(hdc_path))
60
+ name = (getattr(loaded, "name", "") or "").strip() or hdc_path.stem
61
+
62
+ existing = (
63
+ self.db.query(CharacterDef)
64
+ .filter_by(tenant_id=self.tenant_id, source=source,
65
+ hdc_filename=hdc_path.name)
66
+ .first()
67
+ )
68
+ if existing:
69
+ self.db.delete(existing)
70
+ self.db.flush()
71
+ character = CharacterDef(
72
+ tenant_id=self.tenant_id, owner_id=self.owner_id, source=source,
73
+ hdc_filename=hdc_path.name, name=name, is_npc=is_npc,
74
+ )
75
+ self.db.add(character)
76
+ self.db.flush()
77
+ persist_loaded(self.db, character, loaded)
78
+ self._populate_categories(character)
79
+ return character
80
+
81
+ def _populate_categories(self, character: CharacterDef) -> None:
82
+ """Label the freshly-persisted objects with special-effect categories.
83
+
84
+ Runs after the objects exist so it can read them relationally. Never
85
+ raises: categorisation is a labelling pass layered on top of import, and
86
+ a generation-service outage must not lose a character that already parsed and
87
+ costed correctly. Without a bearer this is a no-op and pools simply stay
88
+ unrestricted.
89
+ """
90
+ if not self.category_bearer:
91
+ return
92
+ from kirby.builds.power_categories import (
93
+ populate_categories_for_character,
94
+ )
95
+
96
+ try:
97
+ populate_categories_for_character(
98
+ self.db, character.id, bearer=self.category_bearer)
99
+ except Exception: # noqa: BLE001 — never fail an import over labelling
100
+ logger.warning("category population failed for %s", character.id,
101
+ exc_info=True)
102
+
103
+
104
+ # ---------------------------------------------------------------- bootstrap
105
+
106
+ def _resolve_tenant(db: Session, slug: str) -> UUID:
107
+ t = db.query(Tenant).filter_by(slug=slug).first()
108
+ if t is None:
109
+ raise ValueError(f"Tenant {slug!r} not found in DB")
110
+ return t.id
111
+
112
+
113
+ def _resolve_owner(db: Session, sub: str = "local-bootstrap") -> UUID:
114
+ u = db.query(User).filter_by(sub=sub).first()
115
+ if u is None:
116
+ raise ValueError(f"User with sub={sub!r} not found in DB")
117
+ return u.id
@@ -113,6 +113,12 @@ class CharacterObject(Base):
113
113
  private: Mapped[bool | None] = mapped_column(Boolean)
114
114
  sense_group: Mapped[str | None] = mapped_column(String(32))
115
115
  sense_active: Mapped[bool | None] = mapped_column(Boolean)
116
+ # Resistant Protection's PD/ED/Mental/Power split. Cost is by LEVELS, so
117
+ # these move no points; they say which half is physical.
118
+ pd_levels: Mapped[int | None] = mapped_column(Integer)
119
+ ed_levels: Mapped[int | None] = mapped_column(Integer)
120
+ md_levels: Mapped[int | None] = mapped_column(Integer)
121
+ powd_levels: Mapped[int | None] = mapped_column(Integer)
116
122
 
117
123
  # build doc carried an explicit base_cost (engine emits 0.0 only when BASECOST was
118
124
  # explicit in the source XML); reconstruction must re-emit it
@@ -196,6 +202,9 @@ class ObjectSkillMode(Base):
196
202
  levels_only: Mapped[bool | None] = mapped_column(Boolean)
197
203
  everyman: Mapped[bool | None] = mapped_column(Boolean)
198
204
  characteristic: Mapped[str | None] = mapped_column(String(32))
205
+ # A Language's NATIVE_TONGUE (it is free). NULL on every other skill: the
206
+ # engine writes the key on a Language only.
207
+ native_tongue: Mapped[bool | None] = mapped_column(Boolean)
199
208
 
200
209
 
201
210
  class ObjectManeuver(Base):
@@ -45,7 +45,6 @@ from kirby.engines.cost.hdc import build_loader
45
45
  from kirby.auth.models import User
46
46
  from kirby.characters.models import CharacterDef
47
47
  from kirby.builds.models import (
48
- CharacterObject,
49
48
  PowerLibrary,
50
49
  )
51
50
  from kirby.shared.db import SessionLocal
@@ -167,104 +166,80 @@ def ensure_owner_character(
167
166
  return char
168
167
 
169
168
 
170
- # ── object_type classifier ──────────────────────────────────────────────────
169
+ # ── the engine's document for a file ───────────────────────────────────────
171
170
 
172
171
 
173
- def classify_object_type(obj: Any) -> str:
174
- """character_objects.object_type ∈ {power, modifier, adder, ...}."""
175
- cls_name = type(obj).__name__.lower()
176
- # Modifier class hierarchy in kirby_cost: anything that's an advantage
177
- # or limitation is a Modifier subclass. Specific check by XMLID later.
178
- if "modifier" in cls_name or "advantage" in cls_name or "limitation" in cls_name:
179
- return "modifier"
180
- if "adder" in cls_name:
181
- return "adder"
182
- # XMLIDs for canonical modifiers
183
- return "power"
184
-
185
-
186
- # ── recursive insertion ─────────────────────────────────────────────────────
187
-
172
+ @dataclass
173
+ class FileDoc:
174
+ """One loaded file's build doc, indexed for cataloguing single powers.
188
175
 
189
- def insert_object_subtree(
176
+ ``doc_of`` maps an engine object (by identity) to its doc object, so a
177
+ power picked off ``hero.powers`` finds the document the engine wrote for
178
+ it. ``costs`` is the per-object cost cache keyed by doc id.
179
+ """
180
+ doc_of: dict[int, dict[str, Any]]
181
+ flat: list[dict[str, Any]]
182
+ costs: dict[str, dict[str, float]]
183
+
184
+
185
+ def file_doc(hero: Any) -> FileDoc:
186
+ """``to_build_json`` emits one doc object per engine object, in order, so
187
+ the two lists zip. A length mismatch means that stopped being true, and
188
+ cataloguing against a misaligned index would file costs under the wrong
189
+ powers — so it raises instead."""
190
+ from kirby_cost.io import to_build_json
191
+
192
+ doc = to_build_json(hero)
193
+ doc_of: dict[int, dict[str, Any]] = {}
194
+ flat: list[dict[str, Any]] = []
195
+ costs: dict[str, dict[str, float]] = {}
196
+ for section in ("powers", "equipment"):
197
+ objs = list(getattr(hero, section, []) or [])
198
+ docs = doc.get(section) or []
199
+ if len(objs) != len(docs):
200
+ raise RuntimeError(
201
+ f"{section}: {len(objs)} engine objects but {len(docs)} doc objects")
202
+ for o, d in zip(objs, docs):
203
+ doc_of[id(o)] = d
204
+ flat.append(d)
205
+ costs[d["id"]] = {
206
+ "base_cost": float(getattr(o, "base_cost", 0) or 0),
207
+ "active_cost": float(getattr(o, "active_cost", 0) or 0),
208
+ "real_cost": float(getattr(o, "real_cost", 0) or 0),
209
+ }
210
+ return FileDoc(doc_of=doc_of, flat=flat, costs=costs)
211
+
212
+
213
+ # ── insertion ───────────────────────────────────────────────────────────────
214
+
215
+
216
+ def insert_power_subtree(
190
217
  db: Session,
191
- obj: Any,
218
+ d: dict[str, Any],
192
219
  *,
220
+ fdoc: FileDoc,
193
221
  character_id: uuid.UUID,
194
- parent_id: int | None,
195
- position: int,
196
- object_type: str | None = None,
197
- hero_powers: list[Any] | None = None,
222
+ parent_id: int | None = None,
223
+ position: int = 0,
198
224
  ) -> int:
199
- """Insert one HD object as a ``character_objects`` row, then recurse into
200
- its modifiers, adders, and (for framework powers) child slots. Returns
201
- the inserted row's id so callers can chain it as a parent_id.
202
-
203
- ``hero_powers`` is the full ``hero.powers`` list — required for
204
- Multipower/VPP/ElementalControl slot resolution, because the loader
205
- sets ``slot.parent = framework`` but does NOT back-populate
206
- ``framework.objects``. We walk ``hero_powers`` filtering by
207
- ``slot.parent is obj`` to find slots that belong to ``obj``."""
208
- row = CharacterObject(
209
- character_id=character_id,
210
- parent_id=parent_id,
211
- xmlid=str(obj.xmlid),
212
- object_type=object_type or classify_object_type(obj),
213
- name=(getattr(obj, "name", None) or None) or None,
214
- alias=getattr(obj, "alias", None) or None,
215
- base_cost=float(getattr(obj, "base_cost", 0.0) or 0.0),
216
- levels=int(getattr(obj, "levels", 0) or 0),
217
- active_cost=_safe_float(_safe_attr(obj, "active_cost")),
218
- real_cost=_safe_float(_safe_attr(obj, "real_cost")),
219
- position=position,
220
- input_value=getattr(obj, "input", None) or None,
221
- option_id=_safe_attr(obj, "selected_option") or None,
222
- notes=(getattr(obj, "notes", "") or "").strip() or None,
223
- )
224
- db.add(row)
225
- db.flush()
225
+ """Write one power's doc object through the build-doc writer, then its
226
+ framework slots beneath it. Returns the root row's id.
226
227
 
227
- # Recurse: assigned_modifiers → modifier children
228
- for i, mod in enumerate(getattr(obj, "assigned_modifiers", []) or []):
229
- insert_object_subtree(
230
- db, mod,
231
- character_id=character_id, parent_id=row.id,
232
- position=i, object_type="modifier",
233
- )
234
-
235
- # Recurse: assigned_adders → adder children
236
- for i, add in enumerate(getattr(obj, "assigned_adders", []) or []):
237
- insert_object_subtree(
238
- db, add,
239
- character_id=character_id, parent_id=row.id,
240
- position=i, object_type="adder",
241
- )
228
+ Modifiers, adders, compound sub-powers and an Endurance Reserve's REC are
229
+ nested IN the doc object, so the writer handles them. Multipower / VPP /
230
+ Elemental Control slots — and a LIST's members — are flat doc objects
231
+ whose ``parent`` names the framework, so they are attached here.
232
+ """
233
+ from kirby.builds.build_doc_store import persist_object
242
234
 
243
- # Framework slot resolution (kirby-cost API only):
244
- # - CompoundPower nests its slots on .powers
245
- # - Multipower / VPP / ElementalControl (extend List) DO have slot
246
- # objects with `.parent = framework`, but the loader does NOT
247
- # back-populate `framework.objects` — so we must walk hero.powers
248
- # and filter by `slot.parent is obj`. (Verified 2026-05-12 by
249
- # instrumenting LoadedHero.)
250
- if str(obj.xmlid).upper() in FRAMEWORK_XMLIDS:
251
- nested = getattr(obj, "powers", None) or []
252
- sibling_slots: list[Any] = []
253
- if hero_powers is not None:
254
- sibling_slots = [
255
- p for p in hero_powers if getattr(p, "parent", None) is obj
256
- ]
257
- # Prefer parent-linked siblings when present (MP/VPP/EC case);
258
- # otherwise fall back to nested .powers (CompoundPower case).
259
- slots = sibling_slots if sibling_slots else nested
235
+ row = persist_object(db, character_id, d, object_type="power",
236
+ parent_id=parent_id, position=position,
237
+ costs=fdoc.costs)
238
+ if str(d["xmlid"]).upper() in FRAMEWORK_XMLIDS:
239
+ slots = [s for s in fdoc.flat if s.get("parent") == d["id"]]
260
240
  for i, slot in enumerate(slots):
261
- insert_object_subtree(
262
- db, slot,
263
- character_id=character_id, parent_id=row.id,
264
- position=i, object_type="power",
265
- hero_powers=hero_powers,
266
- )
267
-
241
+ insert_power_subtree(db, slot, fdoc=fdoc, character_id=character_id,
242
+ parent_id=row.id, position=i)
268
243
  return row.id
269
244
 
270
245
 
@@ -324,7 +299,7 @@ def write_library_entry(
324
299
  sfx_tags: list[str],
325
300
  mechanical_tags: list[str],
326
301
  embed_fn: Optional[callable] = None,
327
- hero_powers: list[Any] | None = None,
302
+ fdoc: FileDoc,
328
303
  ) -> Optional[PowerLibrary]:
329
304
  """Insert one POWER's tree into character_objects, then a power_library row.
330
305
 
@@ -354,13 +329,8 @@ def write_library_entry(
354
329
  kind = FRAMEWORK_KIND_MAP.get(xmlid, "single")
355
330
 
356
331
  # Insert the character_objects subtree
357
- root_object_id = insert_object_subtree(
358
- db, power,
359
- character_id=owner_character.id,
360
- parent_id=None,
361
- position=0,
362
- object_type="power",
363
- hero_powers=hero_powers,
332
+ root_object_id = insert_power_subtree(
333
+ db, fdoc.doc_of[id(power)], fdoc=fdoc, character_id=owner_character.id,
364
334
  )
365
335
 
366
336
  description_text = (getattr(power, "notes", "") or "").strip() or None
@@ -448,6 +418,7 @@ def import_file(
448
418
  # them apart on a Hero does not apply to a catalogue entry.
449
419
  hero_powers = list(getattr(hero, "powers", []) or [])
450
420
  hero_powers += list(getattr(hero, "equipment", []) or [])
421
+ fdoc = file_doc(hero)
451
422
  for power in hero_powers:
452
423
  stats.powers_seen += 1
453
424
  xmlid = str(_safe_attr(power, "xmlid", "")).upper()
@@ -483,7 +454,7 @@ def import_file(
483
454
  sfx_tags=sfx_tags,
484
455
  mechanical_tags=mechanical_tags,
485
456
  embed_fn=embed_fn,
486
- hero_powers=hero_powers,
457
+ fdoc=fdoc,
487
458
  )
488
459
  sp.commit()
489
460
  except Exception as e:
@@ -36,7 +36,7 @@ from kirby.campaigns.services.legacy_translation import (
36
36
  )
37
37
  from kirby.characters.models import CharacterDef, CharacterNarrative
38
38
  from kirby.characters._shared import load_character_in_tenant
39
- from kirby.builds.json_exporter import export_character
39
+ from kirby.builds.build_doc_store import build_doc_from_rows
40
40
  from kirby.tenants.routes import _ensure_user_row
41
41
 
42
42
  router = APIRouter(prefix="/characters", tags=["characters"])
@@ -223,10 +223,10 @@ def export_character_json(
223
223
  db: Session = Depends(get_db),
224
224
  download: bool = False,
225
225
  ):
226
- """Export a character as JSON, in the canonical hdc_parser shape.
226
+ """Export a character as its canonical build doc.
227
227
 
228
- The output is the same shape ``JSONImporter.import_file`` reads,
229
- so this is the round-trip transport for save / share / backup.
228
+ The same document ``GET /{id}/build`` returns and ``PUT /{id}/build``
229
+ accepts, so this is the round-trip transport for save / share / backup.
230
230
  Set ``?download=1`` to get a Content-Disposition: attachment
231
231
  header for browser-friendly download.
232
232
 
@@ -241,7 +241,7 @@ def export_character_json(
241
241
  # not-found.
242
242
  raise HTTPException(status_code=404, detail="character not found")
243
243
 
244
- payload = export_character(db, character_id)
244
+ payload = build_doc_from_rows(db, character_id)
245
245
  body = json.dumps(payload, indent=2).encode()
246
246
  headers: dict[str, str] = {}
247
247
  if download:
@@ -252,7 +252,7 @@ def export_character_json(
252
252
  for c in (char.name or str(character_id))
253
253
  ) or str(character_id)
254
254
  headers["Content-Disposition"] = (
255
- f'attachment; filename="{safe}.hdc.json"'
255
+ f'attachment; filename="{safe}.build.json"'
256
256
  )
257
257
  return Response(
258
258
  content=body,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kirby-api
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Kirby — HERO System VTT backend API (FastAPI mono)
5
5
  Author: PeterB
6
6
  License: PolyForm Noncommercial License 1.0.0
@@ -22,7 +22,7 @@ Requires-Dist: cryptography>=44
22
22
  Requires-Dist: python-jose[cryptography]>=3.3
23
23
  Requires-Dist: pyjwt[crypto]>=2.10
24
24
  Requires-Dist: redis>=5.2
25
- Requires-Dist: kirby-cost>=0.6.1
25
+ Requires-Dist: kirby-cost>=0.6.2
26
26
  Requires-Dist: lxml>=5.3
27
27
  Requires-Dist: python-multipart>=0.0.20
28
28
  Requires-Dist: email-validator>=2.2
@@ -106,6 +106,7 @@ kirby/_migrations/versions/20260918_0000_drop_combat_tables.py
106
106
  kirby/_migrations/versions/20260918_0001_harness_tables.py
107
107
  kirby/_migrations/versions/20260918_0002_narration_speaker_id.py
108
108
  kirby/_migrations/versions/20261005_0001_knowledge_indexes.py
109
+ kirby/_migrations/versions/20261005_0002_build_doc_fields.py
109
110
  kirby/_migrations/versions/21980c836860_add_asset_id_refs.py
110
111
  kirby/_migrations/versions/ba28e71c955d_codex_init.py
111
112
  kirby/admin/__init__.py
@@ -123,7 +124,6 @@ kirby/builds/cli.py
123
124
  kirby/builds/codex_linker.py
124
125
  kirby/builds/combatant_loader.py
125
126
  kirby/builds/importer.py
126
- kirby/builds/json_exporter.py
127
127
  kirby/builds/json_importer.py
128
128
  kirby/builds/models.py
129
129
  kirby/builds/power_categories.py
@@ -13,7 +13,7 @@ cryptography>=44
13
13
  python-jose[cryptography]>=3.3
14
14
  pyjwt[crypto]>=2.10
15
15
  redis>=5.2
16
- kirby-cost>=0.6.1
16
+ kirby-cost>=0.6.2
17
17
  lxml>=5.3
18
18
  python-multipart>=0.0.20
19
19
  email-validator>=2.2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kirby-api"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Kirby — HERO System VTT backend API (FastAPI mono)"
5
5
  # Published to public PyPI. Every runtime dependency below resolves from
6
6
  # public PyPI too — kirby-cost, kirby-combat, kirby-dice and kirby-ai all
@@ -91,7 +91,7 @@ dependencies = [
91
91
  # columns stay NULL on every import, and a rebuilt character silently
92
92
  # counts purchases HD keeps out of its totals — Cobra came back DCV 12
93
93
  # where the canonical .hdc load says 10.
94
- "kirby-cost>=0.6.1",
94
+ "kirby-cost>=0.6.2",
95
95
  "lxml>=5.3",
96
96
 
97
97
  # Utilities