lcp 0.1.0

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 (1676) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/skills/lcp-custom-field/SKILL.md +205 -0
  3. data/.claude/skills/lcp-getting-started/SKILL.md +332 -0
  4. data/.claude/skills/lcp-host-binding/SKILL.md +287 -0
  5. data/.claude/skills/lcp-model/SKILL.md +185 -0
  6. data/.claude/skills/lcp-permissions/SKILL.md +176 -0
  7. data/.claude/skills/lcp-presenter/SKILL.md +194 -0
  8. data/.claude/skills/lcp-workflow/SKILL.md +281 -0
  9. data/CHANGELOG.md +69 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +28 -0
  12. data/Rakefile +17 -0
  13. data/app/assets/javascripts/lcp_ruby/application.js +58 -0
  14. data/app/assets/javascripts/lcp_ruby/controllers/advanced_filter_controller.js +1521 -0
  15. data/app/assets/javascripts/lcp_ruby/controllers/approval_actions_controller.js +24 -0
  16. data/app/assets/javascripts/lcp_ruby/controllers/array_input_controller.js +156 -0
  17. data/app/assets/javascripts/lcp_ruby/controllers/batch_select_controller.js +405 -0
  18. data/app/assets/javascripts/lcp_ruby/controllers/cascading_selects_controller.js +436 -0
  19. data/app/assets/javascripts/lcp_ruby/controllers/char_counter_controller.js +28 -0
  20. data/app/assets/javascripts/lcp_ruby/controllers/clipboard_controller.js +62 -0
  21. data/app/assets/javascripts/lcp_ruby/controllers/conditional_rendering_controller.js +178 -0
  22. data/app/assets/javascripts/lcp_ruby/controllers/confirm_dialog_controller.js +131 -0
  23. data/app/assets/javascripts/lcp_ruby/controllers/custom_fields_manage_controller.js +80 -0
  24. data/app/assets/javascripts/lcp_ruby/controllers/dialog_controller.js +421 -0
  25. data/app/assets/javascripts/lcp_ruby/controllers/dialog_nested_controller.js +182 -0
  26. data/app/assets/javascripts/lcp_ruby/controllers/direct_upload_controller.js +100 -0
  27. data/app/assets/javascripts/lcp_ruby/controllers/drawer_controller.js +205 -0
  28. data/app/assets/javascripts/lcp_ruby/controllers/dropdown_controller.js +160 -0
  29. data/app/assets/javascripts/lcp_ruby/controllers/export_dialog_controller.js +15 -0
  30. data/app/assets/javascripts/lcp_ruby/controllers/field_picker_controller.js +290 -0
  31. data/app/assets/javascripts/lcp_ruby/controllers/file_upload_controller.js +135 -0
  32. data/app/assets/javascripts/lcp_ruby/controllers/filter_auto_submit_controller.js +46 -0
  33. data/app/assets/javascripts/lcp_ruby/controllers/filter_dirty_controller.js +94 -0
  34. data/app/assets/javascripts/lcp_ruby/controllers/form_actions_controller.js +68 -0
  35. data/app/assets/javascripts/lcp_ruby/controllers/form_handling_controller.js +67 -0
  36. data/app/assets/javascripts/lcp_ruby/controllers/import_dialog_controller.js +142 -0
  37. data/app/assets/javascripts/lcp_ruby/controllers/import_mapper_controller.js +322 -0
  38. data/app/assets/javascripts/lcp_ruby/controllers/index_sortable_controller.js +388 -0
  39. data/app/assets/javascripts/lcp_ruby/controllers/inline_create_controller.js +137 -0
  40. data/app/assets/javascripts/lcp_ruby/controllers/kanban_board_controller.js +290 -0
  41. data/app/assets/javascripts/lcp_ruby/controllers/menu_controller.js +19 -0
  42. data/app/assets/javascripts/lcp_ruby/controllers/nested_forms_controller.js +261 -0
  43. data/app/assets/javascripts/lcp_ruby/controllers/responsive_sidebar_controller.js +161 -0
  44. data/app/assets/javascripts/lcp_ruby/controllers/responsive_top_nav_controller.js +394 -0
  45. data/app/assets/javascripts/lcp_ruby/controllers/saved_filters_controller.js +129 -0
  46. data/app/assets/javascripts/lcp_ruby/controllers/search_controller.js +82 -0
  47. data/app/assets/javascripts/lcp_ruby/controllers/selection_controller.js +33 -0
  48. data/app/assets/javascripts/lcp_ruby/controllers/sidebar_controller.js +83 -0
  49. data/app/assets/javascripts/lcp_ruby/controllers/sidebar_toggle_controller.js +66 -0
  50. data/app/assets/javascripts/lcp_ruby/controllers/slider_controller.js +26 -0
  51. data/app/assets/javascripts/lcp_ruby/controllers/submenu_controller.js +55 -0
  52. data/app/assets/javascripts/lcp_ruby/controllers/tom_select_controller.js +168 -0
  53. data/app/assets/javascripts/lcp_ruby/controllers/tree_index_controller.js +106 -0
  54. data/app/assets/javascripts/lcp_ruby/controllers/tree_reparent_controller.js +182 -0
  55. data/app/assets/javascripts/lcp_ruby/controllers/tree_select_controller.js +119 -0
  56. data/app/assets/javascripts/lcp_ruby/controllers/ui_components_controller.js +135 -0
  57. data/app/assets/javascripts/lcp_ruby/controllers/zone_tabs_controller.js +66 -0
  58. data/app/assets/javascripts/lcp_ruby/dev_toolbar.js +494 -0
  59. data/app/assets/javascripts/lcp_ruby/i18n.js.erb +29 -0
  60. data/app/assets/javascripts/lcp_ruby/lucide_init.js +24 -0
  61. data/app/assets/javascripts/lcp_ruby/stimulus_bootstrap.js +7 -0
  62. data/app/assets/javascripts/lcp_ruby/utils.js +119 -0
  63. data/app/assets/javascripts/lcp_ruby/xhr_fetch.js +411 -0
  64. data/app/assets/stylesheets/lcp_ruby/application.css +1940 -0
  65. data/app/assets/stylesheets/lcp_ruby/dev_toolbar.css +355 -0
  66. data/app/controllers/concerns/lcp_ruby/dialog_rendering.rb +167 -0
  67. data/app/controllers/concerns/lcp_ruby/form_action_execution.rb +249 -0
  68. data/app/controllers/concerns/lcp_ruby/page_authorization.rb +105 -0
  69. data/app/controllers/concerns/lcp_ruby/zone_resolution.rb +199 -0
  70. data/app/controllers/lcp_ruby/actions_controller.rb +481 -0
  71. data/app/controllers/lcp_ruby/application_controller.rb +59 -0
  72. data/app/controllers/lcp_ruby/approval_tasks_controller.rb +98 -0
  73. data/app/controllers/lcp_ruby/auth/base_controller.rb +58 -0
  74. data/app/controllers/lcp_ruby/auth/callbacks_controller.rb +103 -0
  75. data/app/controllers/lcp_ruby/auth/passwords_controller.rb +15 -0
  76. data/app/controllers/lcp_ruby/auth/registrations_controller.rb +37 -0
  77. data/app/controllers/lcp_ruby/auth/sessions_controller.rb +112 -0
  78. data/app/controllers/lcp_ruby/custom_fields_controller.rb +370 -0
  79. data/app/controllers/lcp_ruby/dev_toolbar_controller.rb +197 -0
  80. data/app/controllers/lcp_ruby/dialogs_controller.rb +199 -0
  81. data/app/controllers/lcp_ruby/health_controller.rb +23 -0
  82. data/app/controllers/lcp_ruby/impersonation_controller.rb +32 -0
  83. data/app/controllers/lcp_ruby/landing_controller.rb +49 -0
  84. data/app/controllers/lcp_ruby/metrics_controller.rb +17 -0
  85. data/app/controllers/lcp_ruby/resources_controller.rb +2218 -0
  86. data/app/controllers/lcp_ruby/saved_filters_controller.rb +221 -0
  87. data/app/helpers/lcp_ruby/condition_helper.rb +87 -0
  88. data/app/helpers/lcp_ruby/dashboard_helper.rb +45 -0
  89. data/app/helpers/lcp_ruby/dev_toolbar_helper.rb +17 -0
  90. data/app/helpers/lcp_ruby/dialog_helper.rb +27 -0
  91. data/app/helpers/lcp_ruby/display/card_helper.rb +317 -0
  92. data/app/helpers/lcp_ruby/display_helper.rb +63 -0
  93. data/app/helpers/lcp_ruby/display_template_helper.rb +100 -0
  94. data/app/helpers/lcp_ruby/form_helper.rb +1020 -0
  95. data/app/helpers/lcp_ruby/grouped_query_helper.rb +107 -0
  96. data/app/helpers/lcp_ruby/i18n_payload_helper.rb +197 -0
  97. data/app/helpers/lcp_ruby/layout_helper.rb +832 -0
  98. data/app/helpers/lcp_ruby/link_through_helper.rb +29 -0
  99. data/app/helpers/lcp_ruby/oidc_button_helper.rb +59 -0
  100. data/app/helpers/lcp_ruby/search_helper.rb +31 -0
  101. data/app/helpers/lcp_ruby/tree_helper.rb +140 -0
  102. data/app/helpers/lcp_ruby/view_slot_helper.rb +33 -0
  103. data/app/models/lcp_ruby/user.rb +61 -0
  104. data/app/views/layouts/lcp_ruby/application.html.erb +168 -0
  105. data/app/views/layouts/lcp_ruby/auth.html.erb +170 -0
  106. data/app/views/lcp_ruby/auth/callbacks/failure.html.erb +18 -0
  107. data/app/views/lcp_ruby/auth/mailer/reset_password_instructions.html.erb +9 -0
  108. data/app/views/lcp_ruby/auth/passwords/edit.html.erb +34 -0
  109. data/app/views/lcp_ruby/auth/passwords/new.html.erb +24 -0
  110. data/app/views/lcp_ruby/auth/registrations/edit.html.erb +48 -0
  111. data/app/views/lcp_ruby/auth/registrations/new.html.erb +46 -0
  112. data/app/views/lcp_ruby/auth/sessions/new.html.erb +55 -0
  113. data/app/views/lcp_ruby/auth/shared/_links.html.erb +13 -0
  114. data/app/views/lcp_ruby/custom_fields/_manage_row.html.erb +82 -0
  115. data/app/views/lcp_ruby/custom_fields/manage.html.erb +47 -0
  116. data/app/views/lcp_ruby/dialogs/_dialog_composite_frame.html.erb +87 -0
  117. data/app/views/lcp_ruby/dialogs/_dialog_frame.html.erb +60 -0
  118. data/app/views/lcp_ruby/dialogs/_dialog_show_frame.html.erb +14 -0
  119. data/app/views/lcp_ruby/dialogs/_show_secret.html.erb +10 -0
  120. data/app/views/lcp_ruby/dialogs/_success.html.erb +1 -0
  121. data/app/views/lcp_ruby/errors/not_found.html.erb +5 -0
  122. data/app/views/lcp_ruby/menu_renderers/_user_menu.html.erb +51 -0
  123. data/app/views/lcp_ruby/menu_renderers/_user_menu_panel.html.erb +32 -0
  124. data/app/views/lcp_ruby/navigation/_auto_top.html.erb +16 -0
  125. data/app/views/lcp_ruby/navigation/_both_sidebar.html.erb +1 -0
  126. data/app/views/lcp_ruby/navigation/_both_top.html.erb +1 -0
  127. data/app/views/lcp_ruby/navigation/_impersonation_banner.html.erb +14 -0
  128. data/app/views/lcp_ruby/navigation/_item_content.html.erb +23 -0
  129. data/app/views/lcp_ruby/navigation/_link_or_button.html.erb +51 -0
  130. data/app/views/lcp_ruby/navigation/_mobile_header.html.erb +23 -0
  131. data/app/views/lcp_ruby/navigation/_panel_item.html.erb +32 -0
  132. data/app/views/lcp_ruby/navigation/_sidebar.html.erb +38 -0
  133. data/app/views/lcp_ruby/navigation/_sidebar_item.html.erb +38 -0
  134. data/app/views/lcp_ruby/navigation/_sidebar_toggle.html.erb +32 -0
  135. data/app/views/lcp_ruby/navigation/_top.html.erb +26 -0
  136. data/app/views/lcp_ruby/navigation/_top_item.html.erb +112 -0
  137. data/app/views/lcp_ruby/navigation/_widget_item.html.erb +14 -0
  138. data/app/views/lcp_ruby/resources/_action_button.html.erb +138 -0
  139. data/app/views/lcp_ruby/resources/_advanced_filter.html.erb +55 -0
  140. data/app/views/lcp_ruby/resources/_api_status_banner.html.erb +11 -0
  141. data/app/views/lcp_ruby/resources/_association_list.html.erb +132 -0
  142. data/app/views/lcp_ruby/resources/_audit_history.html.erb +66 -0
  143. data/app/views/lcp_ruby/resources/_batch_toolbar.html.erb +54 -0
  144. data/app/views/lcp_ruby/resources/_filter_form.html.erb +57 -0
  145. data/app/views/lcp_ruby/resources/_form.html.erb +73 -0
  146. data/app/views/lcp_ruby/resources/_form_action_button.html.erb +20 -0
  147. data/app/views/lcp_ruby/resources/_form_action_dropdown_item.html.erb +17 -0
  148. data/app/views/lcp_ruby/resources/_form_actions.html.erb +38 -0
  149. data/app/views/lcp_ruby/resources/_form_section.html.erb +235 -0
  150. data/app/views/lcp_ruby/resources/_grid_page.html.erb +17 -0
  151. data/app/views/lcp_ruby/resources/_grouped_index.html.erb +61 -0
  152. data/app/views/lcp_ruby/resources/_inline_create_form.html.erb +50 -0
  153. data/app/views/lcp_ruby/resources/_json_items_list.html.erb +77 -0
  154. data/app/views/lcp_ruby/resources/_json_nested_fields.html.erb +30 -0
  155. data/app/views/lcp_ruby/resources/_kanban_card.html.erb +31 -0
  156. data/app/views/lcp_ruby/resources/_kanban_card_body.html.erb +17 -0
  157. data/app/views/lcp_ruby/resources/_kanban_column.html.erb +33 -0
  158. data/app/views/lcp_ruby/resources/_kanban_column_header.html.erb +10 -0
  159. data/app/views/lcp_ruby/resources/_kanban_index.html.erb +17 -0
  160. data/app/views/lcp_ruby/resources/_nested_field_cell.html.erb +33 -0
  161. data/app/views/lcp_ruby/resources/_nested_fields.html.erb +26 -0
  162. data/app/views/lcp_ruby/resources/_nested_row_content.html.erb +65 -0
  163. data/app/views/lcp_ruby/resources/_scope_filters.html.erb +71 -0
  164. data/app/views/lcp_ruby/resources/_semantic_index_page.html.erb +162 -0
  165. data/app/views/lcp_ruby/resources/_semantic_page.html.erb +121 -0
  166. data/app/views/lcp_ruby/resources/_show_sections.html.erb +128 -0
  167. data/app/views/lcp_ruby/resources/_table_index.html.erb +169 -0
  168. data/app/views/lcp_ruby/resources/_tile_card.html.erb +26 -0
  169. data/app/views/lcp_ruby/resources/_tile_card_body.html.erb +19 -0
  170. data/app/views/lcp_ruby/resources/_tiles_index.html.erb +15 -0
  171. data/app/views/lcp_ruby/resources/_transition_button.html.erb +33 -0
  172. data/app/views/lcp_ruby/resources/_tree_index.html.erb +61 -0
  173. data/app/views/lcp_ruby/resources/_view_switcher.html.erb +24 -0
  174. data/app/views/lcp_ruby/resources/edit.html.erb +9 -0
  175. data/app/views/lcp_ruby/resources/index.html.erb +80 -0
  176. data/app/views/lcp_ruby/resources/new.html.erb +9 -0
  177. data/app/views/lcp_ruby/resources/show.html.erb +37 -0
  178. data/app/views/lcp_ruby/shared/_breadcrumbs.html.erb +15 -0
  179. data/app/views/lcp_ruby/shared/_custom_partial_error.html.erb +3 -0
  180. data/app/views/lcp_ruby/shared/_flash_messages.html.erb +19 -0
  181. data/app/views/lcp_ruby/slots/index/_advanced_filter.html.erb +3 -0
  182. data/app/views/lcp_ruby/slots/index/_collection_actions.html.erb +50 -0
  183. data/app/views/lcp_ruby/slots/index/_manage_all.html.erb +4 -0
  184. data/app/views/lcp_ruby/slots/index/_pagination_footer.html.erb +71 -0
  185. data/app/views/lcp_ruby/slots/index/_predefined_filters.html.erb +8 -0
  186. data/app/views/lcp_ruby/slots/index/_saved_filters.html.erb +87 -0
  187. data/app/views/lcp_ruby/slots/index/_search.html.erb +52 -0
  188. data/app/views/lcp_ruby/slots/index/_search_parameter.html.erb +72 -0
  189. data/app/views/lcp_ruby/slots/index/_sort_dropdown.html.erb +21 -0
  190. data/app/views/lcp_ruby/slots/index/_summary_bar.html.erb +22 -0
  191. data/app/views/lcp_ruby/slots/index/_view_switcher.html.erb +1 -0
  192. data/app/views/lcp_ruby/slots/show/_approval_status.html.erb +8 -0
  193. data/app/views/lcp_ruby/slots/show/_back_to_list.html.erb +1 -0
  194. data/app/views/lcp_ruby/slots/show/_copy_url.html.erb +5 -0
  195. data/app/views/lcp_ruby/slots/show/_single_actions.html.erb +4 -0
  196. data/app/views/lcp_ruby/slots/show/_view_switcher.html.erb +1 -0
  197. data/app/views/lcp_ruby/widgets/_approval_status.html.erb +150 -0
  198. data/app/views/lcp_ruby/widgets/_chart.html.erb +36 -0
  199. data/app/views/lcp_ruby/widgets/_embed.html.erb +13 -0
  200. data/app/views/lcp_ruby/widgets/_kpi_card.html.erb +29 -0
  201. data/app/views/lcp_ruby/widgets/_list.html.erb +17 -0
  202. data/app/views/lcp_ruby/widgets/_presenter_zone.html.erb +84 -0
  203. data/app/views/lcp_ruby/widgets/_record_show_zone.html.erb +11 -0
  204. data/app/views/lcp_ruby/widgets/_text.html.erb +3 -0
  205. data/app/views/lcp_ruby/widgets/_workflow_graph.html.erb +32 -0
  206. data/app/views/lcp_ruby/zones/_custom_zone.html.erb +16 -0
  207. data/app/views/lcp_ruby/zones/_error.html.erb +5 -0
  208. data/app/views/lcp_ruby/zones/_zone_frame.html.erb +25 -0
  209. data/app/views/lcp_ruby/zones/_zone_search.html.erb +20 -0
  210. data/config/locales/cs.yml +859 -0
  211. data/config/locales/en.yml +731 -0
  212. data/config/routes.rb +119 -0
  213. data/docs/README.md +225 -0
  214. data/docs/architecture.md +212 -0
  215. data/docs/feature-catalog.md +763 -0
  216. data/docs/feature-catalog.yml +20911 -0
  217. data/docs/getting-started.md +1187 -0
  218. data/docs/guides/action-buttons.md +537 -0
  219. data/docs/guides/adding-locale.md +353 -0
  220. data/docs/guides/api-backed-models.md +478 -0
  221. data/docs/guides/attachments.md +399 -0
  222. data/docs/guides/auditing.md +333 -0
  223. data/docs/guides/batch-actions.md +342 -0
  224. data/docs/guides/composite-pages.md +1290 -0
  225. data/docs/guides/computed-fields.md +350 -0
  226. data/docs/guides/conditional-rendering.md +832 -0
  227. data/docs/guides/custom-actions.md +238 -0
  228. data/docs/guides/custom-fields.md +439 -0
  229. data/docs/guides/custom-renderers.md +234 -0
  230. data/docs/guides/custom-types.md +274 -0
  231. data/docs/guides/dashboards.md +504 -0
  232. data/docs/guides/debugging/README.md +38 -0
  233. data/docs/guides/debugging/controllers.md +147 -0
  234. data/docs/guides/debugging/data.md +165 -0
  235. data/docs/guides/debugging/metadata.md +143 -0
  236. data/docs/guides/debugging/models.md +126 -0
  237. data/docs/guides/debugging/permissions.md +147 -0
  238. data/docs/guides/debugging/presenters.md +151 -0
  239. data/docs/guides/developer-tools.md +418 -0
  240. data/docs/guides/dialogs.md +392 -0
  241. data/docs/guides/display-types.md +1430 -0
  242. data/docs/guides/eager-loading.md +230 -0
  243. data/docs/guides/event-handlers.md +135 -0
  244. data/docs/guides/export.md +305 -0
  245. data/docs/guides/extensibility.md +761 -0
  246. data/docs/guides/groups.md +220 -0
  247. data/docs/guides/hierarchical-authorization.md +427 -0
  248. data/docs/guides/host-application.md +556 -0
  249. data/docs/guides/host-controller-integration.md +473 -0
  250. data/docs/guides/impersonation.md +83 -0
  251. data/docs/guides/import.md +165 -0
  252. data/docs/guides/inherited-permissions.md +459 -0
  253. data/docs/guides/menu.md +373 -0
  254. data/docs/guides/monitoring.md +254 -0
  255. data/docs/guides/oidc-setup.md +399 -0
  256. data/docs/guides/permission-source.md +205 -0
  257. data/docs/guides/permissions.md +364 -0
  258. data/docs/guides/presenters.md +2324 -0
  259. data/docs/guides/record-aliases.md +303 -0
  260. data/docs/guides/rendering-extension-points.md +280 -0
  261. data/docs/guides/role-source.md +288 -0
  262. data/docs/guides/selectbox.md +516 -0
  263. data/docs/guides/sequences.md +291 -0
  264. data/docs/guides/soft-delete.md +460 -0
  265. data/docs/guides/theming.md +129 -0
  266. data/docs/guides/tiles.md +383 -0
  267. data/docs/guides/tree-structures.md +297 -0
  268. data/docs/guides/userstamps.md +288 -0
  269. data/docs/guides/view-groups.md +259 -0
  270. data/docs/guides/view-slots.md +352 -0
  271. data/docs/guides/virtual-columns.md +810 -0
  272. data/docs/guides/workflow.md +692 -0
  273. data/docs/reference/api-backed-models.md +404 -0
  274. data/docs/reference/api-tokens.md +128 -0
  275. data/docs/reference/auditing.md +277 -0
  276. data/docs/reference/boot_lifecycle.md +188 -0
  277. data/docs/reference/cascading_selects.md +189 -0
  278. data/docs/reference/condition-operators.md +445 -0
  279. data/docs/reference/custom-fields.md +483 -0
  280. data/docs/reference/dialogs.md +286 -0
  281. data/docs/reference/doctor.md +168 -0
  282. data/docs/reference/dynamic-references.md +95 -0
  283. data/docs/reference/eager-loading.md +192 -0
  284. data/docs/reference/engine-configuration.md +989 -0
  285. data/docs/reference/export.md +309 -0
  286. data/docs/reference/forms.md +68 -0
  287. data/docs/reference/groups.md +176 -0
  288. data/docs/reference/host-controller-integration.md +342 -0
  289. data/docs/reference/i18n.md +497 -0
  290. data/docs/reference/i18n_check.md +351 -0
  291. data/docs/reference/import.md +260 -0
  292. data/docs/reference/invariant_check.md +216 -0
  293. data/docs/reference/menu.md +985 -0
  294. data/docs/reference/model-dsl.md +1157 -0
  295. data/docs/reference/models.md +2972 -0
  296. data/docs/reference/monitoring.md +222 -0
  297. data/docs/reference/oidc-bearer.md +269 -0
  298. data/docs/reference/oidc.md +407 -0
  299. data/docs/reference/page_filters.md +328 -0
  300. data/docs/reference/pages.md +1375 -0
  301. data/docs/reference/permission-source.md +185 -0
  302. data/docs/reference/permissions.md +715 -0
  303. data/docs/reference/presenter-dsl.md +1719 -0
  304. data/docs/reference/presenters.md +3627 -0
  305. data/docs/reference/role-source.md +227 -0
  306. data/docs/reference/theme-variables.md +139 -0
  307. data/docs/reference/tree-structures.md +374 -0
  308. data/docs/reference/types.md +470 -0
  309. data/docs/reference/view-groups.md +347 -0
  310. data/docs/reference/view-slots.md +228 -0
  311. data/docs/reference/virtual_forms.md +196 -0
  312. data/docs/reference/workflow-approvals.md +387 -0
  313. data/docs/reference/workflow.md +651 -0
  314. data/examples/crm/Gemfile +9 -0
  315. data/examples/crm/Gemfile.lock +417 -0
  316. data/examples/crm/Rakefile +2 -0
  317. data/examples/crm/app/actions/activity/complete.rb +20 -0
  318. data/examples/crm/app/actions/deal/close_won.rb +20 -0
  319. data/examples/crm/app/assets/config/manifest.js +3 -0
  320. data/examples/crm/app/controllers/application_controller.rb +9 -0
  321. data/examples/crm/app/event_handlers/deal/on_stage_change.rb +17 -0
  322. data/examples/crm/app/lcp_services/computed/weighted_deal_value.rb +13 -0
  323. data/examples/crm/app/lcp_services/data_providers/active_contacts_count.rb +14 -0
  324. data/examples/crm/app/lcp_services/data_providers/open_deals_count.rb +14 -0
  325. data/examples/crm/app/lcp_services/data_providers/pending_activities_count.rb +15 -0
  326. data/examples/crm/app/lcp_services/data_providers/pipeline_value.rb +27 -0
  327. data/examples/crm/app/lcp_services/data_providers/won_deals_count.rb +14 -0
  328. data/examples/crm/app/lcp_services/defaults/thirty_days_out.rb +11 -0
  329. data/examples/crm/app/lcp_services/transforms/titlecase.rb +11 -0
  330. data/examples/crm/app/lcp_services/validators/deal_credit_limit.rb +19 -0
  331. data/examples/crm/app/lcp_services/validators/deal_documents_required.rb +17 -0
  332. data/examples/crm/app/renderers/conditional_badge.rb +39 -0
  333. data/examples/crm/bin/rails +4 -0
  334. data/examples/crm/bin/rake +4 -0
  335. data/examples/crm/config/application.rb +31 -0
  336. data/examples/crm/config/boot.rb +2 -0
  337. data/examples/crm/config/database.yml +12 -0
  338. data/examples/crm/config/environment.rb +2 -0
  339. data/examples/crm/config/initializers/lcp_ruby.rb +45 -0
  340. data/examples/crm/config/lcp_ruby/menu.yml +53 -0
  341. data/examples/crm/config/lcp_ruby/models/activity.rb +43 -0
  342. data/examples/crm/config/lcp_ruby/models/city.rb +19 -0
  343. data/examples/crm/config/lcp_ruby/models/company.rb +65 -0
  344. data/examples/crm/config/lcp_ruby/models/contact.rb +62 -0
  345. data/examples/crm/config/lcp_ruby/models/country.rb +22 -0
  346. data/examples/crm/config/lcp_ruby/models/custom_field_definition.rb +60 -0
  347. data/examples/crm/config/lcp_ruby/models/deal.rb +85 -0
  348. data/examples/crm/config/lcp_ruby/models/deal_category.rb +15 -0
  349. data/examples/crm/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
  350. data/examples/crm/config/lcp_ruby/models/region.rb +15 -0
  351. data/examples/crm/config/lcp_ruby/models/saved_filter.rb +50 -0
  352. data/examples/crm/config/lcp_ruby/pages/activity_quick_log.yml +19 -0
  353. data/examples/crm/config/lcp_ruby/pages/company_detail.yml +127 -0
  354. data/examples/crm/config/lcp_ruby/pages/deals_overview.yml +65 -0
  355. data/examples/crm/config/lcp_ruby/permissions/activity.yml +40 -0
  356. data/examples/crm/config/lcp_ruby/permissions/custom_field_definition.yml +16 -0
  357. data/examples/crm/config/lcp_ruby/permissions/deal.yml +54 -0
  358. data/examples/crm/config/lcp_ruby/permissions/default.yml +16 -0
  359. data/examples/crm/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
  360. data/examples/crm/config/lcp_ruby/permissions/saved_filter.yml +27 -0
  361. data/examples/crm/config/lcp_ruby/presenters/activity.rb +140 -0
  362. data/examples/crm/config/lcp_ruby/presenters/activity_quick_form.rb +36 -0
  363. data/examples/crm/config/lcp_ruby/presenters/activity_short.rb +16 -0
  364. data/examples/crm/config/lcp_ruby/presenters/activity_tiles.rb +35 -0
  365. data/examples/crm/config/lcp_ruby/presenters/city.rb +53 -0
  366. data/examples/crm/config/lcp_ruby/presenters/company.rb +147 -0
  367. data/examples/crm/config/lcp_ruby/presenters/company_activities_zone.rb +26 -0
  368. data/examples/crm/config/lcp_ruby/presenters/company_archive.rb +37 -0
  369. data/examples/crm/config/lcp_ruby/presenters/company_contacts_zone.rb +22 -0
  370. data/examples/crm/config/lcp_ruby/presenters/company_deals_zone.rb +36 -0
  371. data/examples/crm/config/lcp_ruby/presenters/company_short.rb +21 -0
  372. data/examples/crm/config/lcp_ruby/presenters/company_show_zone.rb +38 -0
  373. data/examples/crm/config/lcp_ruby/presenters/company_sidebar.rb +15 -0
  374. data/examples/crm/config/lcp_ruby/presenters/company_tiles.rb +35 -0
  375. data/examples/crm/config/lcp_ruby/presenters/contact.rb +120 -0
  376. data/examples/crm/config/lcp_ruby/presenters/contact_quick_form.rb +17 -0
  377. data/examples/crm/config/lcp_ruby/presenters/contact_short.rb +20 -0
  378. data/examples/crm/config/lcp_ruby/presenters/contact_tiles.rb +34 -0
  379. data/examples/crm/config/lcp_ruby/presenters/country.rb +44 -0
  380. data/examples/crm/config/lcp_ruby/presenters/custom_fields.rb +124 -0
  381. data/examples/crm/config/lcp_ruby/presenters/deal.rb +181 -0
  382. data/examples/crm/config/lcp_ruby/presenters/deal_category.rb +46 -0
  383. data/examples/crm/config/lcp_ruby/presenters/deal_overview.rb +6 -0
  384. data/examples/crm/config/lcp_ruby/presenters/deal_pipeline.rb +18 -0
  385. data/examples/crm/config/lcp_ruby/presenters/deal_short.rb +25 -0
  386. data/examples/crm/config/lcp_ruby/presenters/deal_tiles.rb +48 -0
  387. data/examples/crm/config/lcp_ruby/presenters/region.rb +42 -0
  388. data/examples/crm/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
  389. data/examples/crm/config/lcp_ruby/presenters/saved_filters.rb +93 -0
  390. data/examples/crm/config/lcp_ruby/views/activities.yml +16 -0
  391. data/examples/crm/config/lcp_ruby/views/cities.yml +10 -0
  392. data/examples/crm/config/lcp_ruby/views/companies.yml +14 -0
  393. data/examples/crm/config/lcp_ruby/views/contacts.yml +16 -0
  394. data/examples/crm/config/lcp_ruby/views/countries.yml +8 -0
  395. data/examples/crm/config/lcp_ruby/views/custom_fields.rb +9 -0
  396. data/examples/crm/config/lcp_ruby/views/deal_categories.yml +8 -0
  397. data/examples/crm/config/lcp_ruby/views/deals.yml +19 -0
  398. data/examples/crm/config/lcp_ruby/views/pipeline.yml +10 -0
  399. data/examples/crm/config/lcp_ruby/views/regions.yml +10 -0
  400. data/examples/crm/config/lcp_ruby/views/saved_filters.yml +7 -0
  401. data/examples/crm/config/locales/cs.yml +338 -0
  402. data/examples/crm/config/locales/en.yml +353 -0
  403. data/examples/crm/config/routes.rb +4 -0
  404. data/examples/crm/config/storage.yml +3 -0
  405. data/examples/crm/config.ru +2 -0
  406. data/examples/crm/db/migrate/20260219104942_create_active_storage_tables.active_storage.rb +57 -0
  407. data/examples/crm/db/schema.rb +245 -0
  408. data/examples/crm/db/seeds.rb +1111 -0
  409. data/examples/crm/erd.md +163 -0
  410. data/examples/hr/Gemfile +9 -0
  411. data/examples/hr/Gemfile.lock +419 -0
  412. data/examples/hr/Rakefile +6 -0
  413. data/examples/hr/app/actions/asset/assign_asset.rb +20 -0
  414. data/examples/hr/app/actions/asset/return_asset.rb +20 -0
  415. data/examples/hr/app/actions/candidate/advance.rb +32 -0
  416. data/examples/hr/app/actions/candidate/hire.rb +20 -0
  417. data/examples/hr/app/actions/candidate/reject_candidate.rb +20 -0
  418. data/examples/hr/app/actions/expense_claim/approve.rb +21 -0
  419. data/examples/hr/app/actions/expense_claim/reject.rb +21 -0
  420. data/examples/hr/app/actions/expense_claim/submit.rb +20 -0
  421. data/examples/hr/app/actions/interview/complete_interview.rb +20 -0
  422. data/examples/hr/app/actions/leave_request/approve.rb +21 -0
  423. data/examples/hr/app/actions/leave_request/cancel.rb +20 -0
  424. data/examples/hr/app/actions/leave_request/reject.rb +21 -0
  425. data/examples/hr/app/assets/config/manifest.js +1 -0
  426. data/examples/hr/app/assets/stylesheets/application.css +10 -0
  427. data/examples/hr/app/condition_services/is_own_org_unit.rb +14 -0
  428. data/examples/hr/app/condition_services/is_own_record.rb +20 -0
  429. data/examples/hr/app/controllers/application_controller.rb +15 -0
  430. data/examples/hr/app/event_handlers/asset_assignment/on_create.rb +24 -0
  431. data/examples/hr/app/event_handlers/candidate/on_status_change.rb +18 -0
  432. data/examples/hr/app/event_handlers/leave_request/on_status_change.rb +45 -0
  433. data/examples/hr/app/helpers/application_helper.rb +2 -0
  434. data/examples/hr/app/javascript/application.js +1 -0
  435. data/examples/hr/app/jobs/application_job.rb +7 -0
  436. data/examples/hr/app/lcp_services/computed/employee_tenure.rb +28 -0
  437. data/examples/hr/app/lcp_services/computed/leave_remaining.rb +13 -0
  438. data/examples/hr/app/lcp_services/data_providers/headcount_text.rb +14 -0
  439. data/examples/hr/app/lcp_services/data_providers/open_positions_count.rb +14 -0
  440. data/examples/hr/app/lcp_services/data_providers/pending_expenses_count.rb +14 -0
  441. data/examples/hr/app/lcp_services/data_providers/pending_leaves_count.rb +14 -0
  442. data/examples/hr/app/lcp_services/defaults/current_year.rb +11 -0
  443. data/examples/hr/app/lcp_services/transforms/titlecase.rb +11 -0
  444. data/examples/hr/app/lcp_services/validators/expense_receipt_required.rb +17 -0
  445. data/examples/hr/app/lcp_services/validators/leave_balance_check.rb +37 -0
  446. data/examples/hr/app/models/application_record.rb +3 -0
  447. data/examples/hr/app/views/layouts/application.html.erb +29 -0
  448. data/examples/hr/app/views/pwa/manifest.json.erb +22 -0
  449. data/examples/hr/app/views/pwa/service-worker.js +26 -0
  450. data/examples/hr/bin/brakeman +7 -0
  451. data/examples/hr/bin/bundler-audit +6 -0
  452. data/examples/hr/bin/ci +6 -0
  453. data/examples/hr/bin/dev +2 -0
  454. data/examples/hr/bin/docker-entrypoint +8 -0
  455. data/examples/hr/bin/importmap +4 -0
  456. data/examples/hr/bin/jobs +6 -0
  457. data/examples/hr/bin/kamal +27 -0
  458. data/examples/hr/bin/rails +4 -0
  459. data/examples/hr/bin/rake +4 -0
  460. data/examples/hr/bin/rubocop +8 -0
  461. data/examples/hr/bin/setup +35 -0
  462. data/examples/hr/bin/thrust +5 -0
  463. data/examples/hr/config/application.rb +31 -0
  464. data/examples/hr/config/boot.rb +2 -0
  465. data/examples/hr/config/bundler-audit.yml +5 -0
  466. data/examples/hr/config/cache.yml +16 -0
  467. data/examples/hr/config/ci.rb +20 -0
  468. data/examples/hr/config/credentials.yml.enc +1 -0
  469. data/examples/hr/config/database.yml +36 -0
  470. data/examples/hr/config/deploy.yml +119 -0
  471. data/examples/hr/config/environment.rb +5 -0
  472. data/examples/hr/config/environments/development.rb +66 -0
  473. data/examples/hr/config/environments/production.rb +74 -0
  474. data/examples/hr/config/environments/test.rb +45 -0
  475. data/examples/hr/config/importmap.rb +3 -0
  476. data/examples/hr/config/initializers/assets.rb +7 -0
  477. data/examples/hr/config/initializers/content_security_policy.rb +29 -0
  478. data/examples/hr/config/initializers/filter_parameter_logging.rb +8 -0
  479. data/examples/hr/config/initializers/inflections.rb +16 -0
  480. data/examples/hr/config/initializers/lcp_ruby.rb +18 -0
  481. data/examples/hr/config/lcp_ruby/menu.yml +86 -0
  482. data/examples/hr/config/lcp_ruby/models/announcement.rb +33 -0
  483. data/examples/hr/config/lcp_ruby/models/asset.rb +73 -0
  484. data/examples/hr/config/lcp_ruby/models/asset_assignment.rb +39 -0
  485. data/examples/hr/config/lcp_ruby/models/audit_log.yml +57 -0
  486. data/examples/hr/config/lcp_ruby/models/candidate.rb +68 -0
  487. data/examples/hr/config/lcp_ruby/models/custom_field_definition.rb +60 -0
  488. data/examples/hr/config/lcp_ruby/models/dashboard.rb +17 -0
  489. data/examples/hr/config/lcp_ruby/models/document.rb +38 -0
  490. data/examples/hr/config/lcp_ruby/models/employee.rb +128 -0
  491. data/examples/hr/config/lcp_ruby/models/employee_skill.rb +29 -0
  492. data/examples/hr/config/lcp_ruby/models/expense_claim.rb +70 -0
  493. data/examples/hr/config/lcp_ruby/models/goal.rb +48 -0
  494. data/examples/hr/config/lcp_ruby/models/group.rb +37 -0
  495. data/examples/hr/config/lcp_ruby/models/group_membership.rb +26 -0
  496. data/examples/hr/config/lcp_ruby/models/interview.rb +54 -0
  497. data/examples/hr/config/lcp_ruby/models/job_posting.rb +67 -0
  498. data/examples/hr/config/lcp_ruby/models/leave_balance.rb +31 -0
  499. data/examples/hr/config/lcp_ruby/models/leave_request.rb +52 -0
  500. data/examples/hr/config/lcp_ruby/models/leave_type.rb +35 -0
  501. data/examples/hr/config/lcp_ruby/models/organization_unit.rb +42 -0
  502. data/examples/hr/config/lcp_ruby/models/performance_review.rb +59 -0
  503. data/examples/hr/config/lcp_ruby/models/position.rb +43 -0
  504. data/examples/hr/config/lcp_ruby/models/skill.rb +27 -0
  505. data/examples/hr/config/lcp_ruby/models/training_course.rb +53 -0
  506. data/examples/hr/config/lcp_ruby/models/training_enrollment.rb +35 -0
  507. data/examples/hr/config/lcp_ruby/pages/dashboard.yml +101 -0
  508. data/examples/hr/config/lcp_ruby/permissions/announcement.yml +25 -0
  509. data/examples/hr/config/lcp_ruby/permissions/asset.yml +35 -0
  510. data/examples/hr/config/lcp_ruby/permissions/audit_log.yml +28 -0
  511. data/examples/hr/config/lcp_ruby/permissions/candidate.yml +25 -0
  512. data/examples/hr/config/lcp_ruby/permissions/custom_field_definition.yml +28 -0
  513. data/examples/hr/config/lcp_ruby/permissions/dashboard.yml +25 -0
  514. data/examples/hr/config/lcp_ruby/permissions/default.yml +25 -0
  515. data/examples/hr/config/lcp_ruby/permissions/document.yml +37 -0
  516. data/examples/hr/config/lcp_ruby/permissions/employee.yml +55 -0
  517. data/examples/hr/config/lcp_ruby/permissions/expense_claim.yml +45 -0
  518. data/examples/hr/config/lcp_ruby/permissions/group.yml +27 -0
  519. data/examples/hr/config/lcp_ruby/permissions/job_posting.yml +34 -0
  520. data/examples/hr/config/lcp_ruby/permissions/leave_request.yml +45 -0
  521. data/examples/hr/config/lcp_ruby/permissions/performance_review.yml +42 -0
  522. data/examples/hr/config/lcp_ruby/presenters/announcement.rb +47 -0
  523. data/examples/hr/config/lcp_ruby/presenters/asset.rb +69 -0
  524. data/examples/hr/config/lcp_ruby/presenters/asset_assignment.rb +47 -0
  525. data/examples/hr/config/lcp_ruby/presenters/audit_logs.yml +43 -0
  526. data/examples/hr/config/lcp_ruby/presenters/candidate.rb +71 -0
  527. data/examples/hr/config/lcp_ruby/presenters/custom_fields.rb +124 -0
  528. data/examples/hr/config/lcp_ruby/presenters/dashboard.rb +37 -0
  529. data/examples/hr/config/lcp_ruby/presenters/document.rb +46 -0
  530. data/examples/hr/config/lcp_ruby/presenters/employee.rb +167 -0
  531. data/examples/hr/config/lcp_ruby/presenters/employee_archive.rb +38 -0
  532. data/examples/hr/config/lcp_ruby/presenters/employee_directory.rb +27 -0
  533. data/examples/hr/config/lcp_ruby/presenters/employee_skill.rb +57 -0
  534. data/examples/hr/config/lcp_ruby/presenters/expense_claim.rb +76 -0
  535. data/examples/hr/config/lcp_ruby/presenters/goal.rb +60 -0
  536. data/examples/hr/config/lcp_ruby/presenters/group.rb +48 -0
  537. data/examples/hr/config/lcp_ruby/presenters/interview.rb +59 -0
  538. data/examples/hr/config/lcp_ruby/presenters/job_posting.rb +73 -0
  539. data/examples/hr/config/lcp_ruby/presenters/leave_balance.rb +50 -0
  540. data/examples/hr/config/lcp_ruby/presenters/leave_request.rb +89 -0
  541. data/examples/hr/config/lcp_ruby/presenters/leave_type.rb +52 -0
  542. data/examples/hr/config/lcp_ruby/presenters/organization_unit.rb +56 -0
  543. data/examples/hr/config/lcp_ruby/presenters/performance_review.rb +87 -0
  544. data/examples/hr/config/lcp_ruby/presenters/position.rb +54 -0
  545. data/examples/hr/config/lcp_ruby/presenters/skill.rb +45 -0
  546. data/examples/hr/config/lcp_ruby/presenters/training_course.rb +61 -0
  547. data/examples/hr/config/lcp_ruby/presenters/training_enrollment.rb +49 -0
  548. data/examples/hr/config/lcp_ruby/views/announcements.yml +8 -0
  549. data/examples/hr/config/lcp_ruby/views/asset_assignments.yml +10 -0
  550. data/examples/hr/config/lcp_ruby/views/assets.yml +8 -0
  551. data/examples/hr/config/lcp_ruby/views/audit_logs.yml +8 -0
  552. data/examples/hr/config/lcp_ruby/views/candidates.yml +8 -0
  553. data/examples/hr/config/lcp_ruby/views/custom_fields.yml +8 -0
  554. data/examples/hr/config/lcp_ruby/views/dashboard.yml +6 -0
  555. data/examples/hr/config/lcp_ruby/views/documents.yml +10 -0
  556. data/examples/hr/config/lcp_ruby/views/employee_skills.yml +10 -0
  557. data/examples/hr/config/lcp_ruby/views/employees.yml +14 -0
  558. data/examples/hr/config/lcp_ruby/views/expense_claims.yml +10 -0
  559. data/examples/hr/config/lcp_ruby/views/goals.yml +10 -0
  560. data/examples/hr/config/lcp_ruby/views/groups.yml +8 -0
  561. data/examples/hr/config/lcp_ruby/views/interviews.yml +8 -0
  562. data/examples/hr/config/lcp_ruby/views/job_postings.yml +6 -0
  563. data/examples/hr/config/lcp_ruby/views/leave_balances.yml +10 -0
  564. data/examples/hr/config/lcp_ruby/views/leave_requests.yml +10 -0
  565. data/examples/hr/config/lcp_ruby/views/leave_types.yml +8 -0
  566. data/examples/hr/config/lcp_ruby/views/organization_units.yml +8 -0
  567. data/examples/hr/config/lcp_ruby/views/performance_reviews.yml +10 -0
  568. data/examples/hr/config/lcp_ruby/views/positions.yml +8 -0
  569. data/examples/hr/config/lcp_ruby/views/skills.yml +8 -0
  570. data/examples/hr/config/lcp_ruby/views/training_courses.yml +8 -0
  571. data/examples/hr/config/lcp_ruby/views/training_enrollments.yml +10 -0
  572. data/examples/hr/config/locales/cs.yml +496 -0
  573. data/examples/hr/config/locales/en.yml +740 -0
  574. data/examples/hr/config/locales/sk.yml +496 -0
  575. data/examples/hr/config/puma.rb +42 -0
  576. data/examples/hr/config/queue.yml +18 -0
  577. data/examples/hr/config/recurring.yml +15 -0
  578. data/examples/hr/config/routes.rb +4 -0
  579. data/examples/hr/config/storage.yml +27 -0
  580. data/examples/hr/config.ru +6 -0
  581. data/examples/hr/db/cache_schema.rb +12 -0
  582. data/examples/hr/db/migrate/20260303202825_create_active_storage_tables.active_storage.rb +57 -0
  583. data/examples/hr/db/queue_schema.rb +129 -0
  584. data/examples/hr/db/schema.rb +588 -0
  585. data/examples/hr/db/seeds.rb +932 -0
  586. data/examples/hr/erd.md +396 -0
  587. data/examples/hr/public/400.html +135 -0
  588. data/examples/hr/public/404.html +135 -0
  589. data/examples/hr/public/406-unsupported-browser.html +135 -0
  590. data/examples/hr/public/422.html +135 -0
  591. data/examples/hr/public/500.html +135 -0
  592. data/examples/hr/public/icon.svg +3 -0
  593. data/examples/hr/public/robots.txt +1 -0
  594. data/examples/showcase/Gemfile +15 -0
  595. data/examples/showcase/Gemfile.lock +425 -0
  596. data/examples/showcase/Rakefile +2 -0
  597. data/examples/showcase/app/actions/showcase_batch_task/assign_batch.rb +20 -0
  598. data/examples/showcase/app/actions/showcase_batch_task/close_task.rb +21 -0
  599. data/examples/showcase/app/actions/showcase_condition/approve.rb +20 -0
  600. data/examples/showcase/app/actions/showcase_permission/lock.rb +20 -0
  601. data/examples/showcase/app/assets/config/manifest.js +3 -0
  602. data/examples/showcase/app/assets/stylesheets/application.css +27 -0
  603. data/examples/showcase/app/condition_services/budget_threshold.rb +16 -0
  604. data/examples/showcase/app/condition_services/overdue_check.rb +11 -0
  605. data/examples/showcase/app/controllers/application_controller.rb +2 -0
  606. data/examples/showcase/app/controllers/docs_controller.rb +48 -0
  607. data/examples/showcase/app/controllers/host_inventory_items_managed_controller.rb +90 -0
  608. data/examples/showcase/app/controllers/host_inventory_items_report_controller.rb +39 -0
  609. data/examples/showcase/app/controllers/host_inventory_items_wizard_controller.rb +50 -0
  610. data/examples/showcase/app/data_providers/showcase_amount_provider.rb +52 -0
  611. data/examples/showcase/app/data_providers/weather_station_provider.rb +140 -0
  612. data/examples/showcase/app/event_handlers/showcase_model/on_status_change.rb +17 -0
  613. data/examples/showcase/app/event_handlers/showcase_permission/on_status_change.rb +17 -0
  614. data/examples/showcase/app/event_handlers/showcase_workflow/log_review_exit.rb +16 -0
  615. data/examples/showcase/app/event_handlers/showcase_workflow/notify_reviewers.rb +15 -0
  616. data/examples/showcase/app/event_handlers/showcase_workflow/request_submitted.rb +15 -0
  617. data/examples/showcase/app/lcp_metrics/showcase_metrics.rb +31 -0
  618. data/examples/showcase/app/lcp_services/computed/showcase_score.rb +18 -0
  619. data/examples/showcase/app/lcp_services/computed/showcase_total.rb +13 -0
  620. data/examples/showcase/app/lcp_services/defaults/one_week_from_now.rb +11 -0
  621. data/examples/showcase/app/lcp_services/menu_items/recent_announcements.rb +36 -0
  622. data/examples/showcase/app/lcp_services/virtual_columns/project_health.rb +26 -0
  623. data/examples/showcase/app/model_extensions/lcp_error_log_extension.rb +12 -0
  624. data/examples/showcase/app/models/host_inventory_item.rb +20 -0
  625. data/examples/showcase/app/models/platform_profile.rb +27 -0
  626. data/examples/showcase/app/models/platform_user.rb +5 -0
  627. data/examples/showcase/app/views/docs/show.html.erb +19 -0
  628. data/examples/showcase/app/views/host_inventory_items_report/index.html.erb +61 -0
  629. data/examples/showcase/app/views/host_inventory_items_report/show.html.erb +39 -0
  630. data/examples/showcase/app/views/layouts/docs.html.erb +46 -0
  631. data/examples/showcase/app/views/showcase/menu_renderers/_status_pill.html.erb +17 -0
  632. data/examples/showcase/app/views/showcase_custom/_activity_timeline.html.erb +37 -0
  633. data/examples/showcase/app/views/showcase_custom/_card_index.html.erb +74 -0
  634. data/examples/showcase/app/views/showcase_custom/_detail_show.html.erb +115 -0
  635. data/examples/showcase/app/views/showcase_custom/_location_map.html.erb +29 -0
  636. data/examples/showcase/app/views/showcase_custom/_location_view.html.erb +34 -0
  637. data/examples/showcase/app/views/showcase_custom/_quick_stats.html.erb +50 -0
  638. data/examples/showcase/app/views/showcase_custom/_stats_sidebar.html.erb +42 -0
  639. data/examples/showcase/app/views/showcase_custom/_tags_editor.html.erb +48 -0
  640. data/examples/showcase/bin/rails +4 -0
  641. data/examples/showcase/bin/rake +4 -0
  642. data/examples/showcase/config/application.rb +37 -0
  643. data/examples/showcase/config/boot.rb +2 -0
  644. data/examples/showcase/config/database.yml +12 -0
  645. data/examples/showcase/config/environment.rb +2 -0
  646. data/examples/showcase/config/initializers/devise.rb +5 -0
  647. data/examples/showcase/config/initializers/lcp_ruby.rb +175 -0
  648. data/examples/showcase/config/lcp_ruby/auth.yml +60 -0
  649. data/examples/showcase/config/lcp_ruby/jobs/data_import.yml +7 -0
  650. data/examples/showcase/config/lcp_ruby/jobs/showcase_cleanup.yml +11 -0
  651. data/examples/showcase/config/lcp_ruby/jobs/showcase_event_triggered.yml +11 -0
  652. data/examples/showcase/config/lcp_ruby/jobs/showcase_multi_step.yml +9 -0
  653. data/examples/showcase/config/lcp_ruby/jobs/showcase_webhook.yml +13 -0
  654. data/examples/showcase/config/lcp_ruby/menu.yml +249 -0
  655. data/examples/showcase/config/lcp_ruby/models/_base_document.rb +26 -0
  656. data/examples/showcase/config/lcp_ruby/models/_categorized_document.rb +19 -0
  657. data/examples/showcase/config/lcp_ruby/models/_contactable.rb +20 -0
  658. data/examples/showcase/config/lcp_ruby/models/api_token.rb +23 -0
  659. data/examples/showcase/config/lcp_ruby/models/article.rb +49 -0
  660. data/examples/showcase/config/lcp_ruby/models/article_tag.rb +10 -0
  661. data/examples/showcase/config/lcp_ruby/models/author.rb +16 -0
  662. data/examples/showcase/config/lcp_ruby/models/batch_operation.yml +79 -0
  663. data/examples/showcase/config/lcp_ruby/models/batch_operation_item.yml +45 -0
  664. data/examples/showcase/config/lcp_ruby/models/category.rb +23 -0
  665. data/examples/showcase/config/lcp_ruby/models/comment.rb +21 -0
  666. data/examples/showcase/config/lcp_ruby/models/custom_field_definition.rb +59 -0
  667. data/examples/showcase/config/lcp_ruby/models/department.rb +32 -0
  668. data/examples/showcase/config/lcp_ruby/models/employee.rb +52 -0
  669. data/examples/showcase/config/lcp_ruby/models/employee_emergency_contact.rb +24 -0
  670. data/examples/showcase/config/lcp_ruby/models/employee_profile.rb +18 -0
  671. data/examples/showcase/config/lcp_ruby/models/employee_skill.rb +10 -0
  672. data/examples/showcase/config/lcp_ruby/models/export_log.yml +27 -0
  673. data/examples/showcase/config/lcp_ruby/models/export_profile.yml +37 -0
  674. data/examples/showcase/config/lcp_ruby/models/feature.rb +76 -0
  675. data/examples/showcase/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
  676. data/examples/showcase/config/lcp_ruby/models/group.rb +26 -0
  677. data/examples/showcase/config/lcp_ruby/models/group_membership.rb +21 -0
  678. data/examples/showcase/config/lcp_ruby/models/group_role_mapping.rb +14 -0
  679. data/examples/showcase/config/lcp_ruby/models/host_inventory_item.yml +111 -0
  680. data/examples/showcase/config/lcp_ruby/models/import_profile.rb +30 -0
  681. data/examples/showcase/config/lcp_ruby/models/import_row.rb +32 -0
  682. data/examples/showcase/config/lcp_ruby/models/ingredient_def.rb +11 -0
  683. data/examples/showcase/config/lcp_ruby/models/lcp_error_log.yml +61 -0
  684. data/examples/showcase/config/lcp_ruby/models/page_config.rb +19 -0
  685. data/examples/showcase/config/lcp_ruby/models/permission_config.rb +21 -0
  686. data/examples/showcase/config/lcp_ruby/models/pipeline.rb +15 -0
  687. data/examples/showcase/config/lcp_ruby/models/pipeline_stage.rb +17 -0
  688. data/examples/showcase/config/lcp_ruby/models/platform_profile.rb +104 -0
  689. data/examples/showcase/config/lcp_ruby/models/profile_setting.rb +17 -0
  690. data/examples/showcase/config/lcp_ruby/models/profile_tag.rb +19 -0
  691. data/examples/showcase/config/lcp_ruby/models/project.rb +23 -0
  692. data/examples/showcase/config/lcp_ruby/models/role.rb +24 -0
  693. data/examples/showcase/config/lcp_ruby/models/saved_filter.rb +50 -0
  694. data/examples/showcase/config/lcp_ruby/models/showcase_aggregate.rb +93 -0
  695. data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_company.rb +14 -0
  696. data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_item.rb +25 -0
  697. data/examples/showcase/config/lcp_ruby/models/showcase_announcement.rb +20 -0
  698. data/examples/showcase/config/lcp_ruby/models/showcase_array.rb +45 -0
  699. data/examples/showcase/config/lcp_ruby/models/showcase_attachment.rb +49 -0
  700. data/examples/showcase/config/lcp_ruby/models/showcase_audit_log.yml +44 -0
  701. data/examples/showcase/config/lcp_ruby/models/showcase_audited_record.rb +20 -0
  702. data/examples/showcase/config/lcp_ruby/models/showcase_batch_task.rb +27 -0
  703. data/examples/showcase/config/lcp_ruby/models/showcase_business_unit.rb +26 -0
  704. data/examples/showcase/config/lcp_ruby/models/showcase_condition.rb +24 -0
  705. data/examples/showcase/config/lcp_ruby/models/showcase_condition_category.rb +16 -0
  706. data/examples/showcase/config/lcp_ruby/models/showcase_condition_task.rb +16 -0
  707. data/examples/showcase/config/lcp_ruby/models/showcase_condition_threshold.rb +13 -0
  708. data/examples/showcase/config/lcp_ruby/models/showcase_contact.rb +22 -0
  709. data/examples/showcase/config/lcp_ruby/models/showcase_custom_render.rb +38 -0
  710. data/examples/showcase/config/lcp_ruby/models/showcase_delete_reason.rb +11 -0
  711. data/examples/showcase/config/lcp_ruby/models/showcase_division.rb +31 -0
  712. data/examples/showcase/config/lcp_ruby/models/showcase_extensibility.rb +25 -0
  713. data/examples/showcase/config/lcp_ruby/models/showcase_field.rb +53 -0
  714. data/examples/showcase/config/lcp_ruby/models/showcase_form.rb +23 -0
  715. data/examples/showcase/config/lcp_ruby/models/showcase_form_action.rb +38 -0
  716. data/examples/showcase/config/lcp_ruby/models/showcase_grade.rb +18 -0
  717. data/examples/showcase/config/lcp_ruby/models/showcase_hr_employee.rb +90 -0
  718. data/examples/showcase/config/lcp_ruby/models/showcase_item_class.rb +31 -0
  719. data/examples/showcase/config/lcp_ruby/models/showcase_job_execution.rb +49 -0
  720. data/examples/showcase/config/lcp_ruby/models/showcase_memo.rb +23 -0
  721. data/examples/showcase/config/lcp_ruby/models/showcase_model.rb +74 -0
  722. data/examples/showcase/config/lcp_ruby/models/showcase_organization.rb +31 -0
  723. data/examples/showcase/config/lcp_ruby/models/showcase_permission.rb +30 -0
  724. data/examples/showcase/config/lcp_ruby/models/showcase_person.rb +23 -0
  725. data/examples/showcase/config/lcp_ruby/models/showcase_positioning.rb +19 -0
  726. data/examples/showcase/config/lcp_ruby/models/showcase_quick_note.rb +13 -0
  727. data/examples/showcase/config/lcp_ruby/models/showcase_recipe.rb +17 -0
  728. data/examples/showcase/config/lcp_ruby/models/showcase_report.rb +32 -0
  729. data/examples/showcase/config/lcp_ruby/models/showcase_school_class.rb +17 -0
  730. data/examples/showcase/config/lcp_ruby/models/showcase_search.rb +84 -0
  731. data/examples/showcase/config/lcp_ruby/models/showcase_sequence.rb +46 -0
  732. data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete.rb +30 -0
  733. data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete_item.rb +21 -0
  734. data/examples/showcase/config/lcp_ruby/models/showcase_student.rb +17 -0
  735. data/examples/showcase/config/lcp_ruby/models/showcase_type_default.rb +49 -0
  736. data/examples/showcase/config/lcp_ruby/models/showcase_userstamps.rb +26 -0
  737. data/examples/showcase/config/lcp_ruby/models/showcase_virtual_field.rb +68 -0
  738. data/examples/showcase/config/lcp_ruby/models/showcase_workflow.rb +54 -0
  739. data/examples/showcase/config/lcp_ruby/models/skill.rb +22 -0
  740. data/examples/showcase/config/lcp_ruby/models/tag.rb +16 -0
  741. data/examples/showcase/config/lcp_ruby/models/user.rb +54 -0
  742. data/examples/showcase/config/lcp_ruby/models/weather_station.rb +20 -0
  743. data/examples/showcase/config/lcp_ruby/models/workflow_approval_request.yml +77 -0
  744. data/examples/showcase/config/lcp_ruby/models/workflow_approval_step.yml +56 -0
  745. data/examples/showcase/config/lcp_ruby/models/workflow_approval_task.yml +51 -0
  746. data/examples/showcase/config/lcp_ruby/models/workflow_audit_log.yml +74 -0
  747. data/examples/showcase/config/lcp_ruby/pages/article_detail.yml +32 -0
  748. data/examples/showcase/config/lcp_ruby/pages/author_detail.yml +27 -0
  749. data/examples/showcase/config/lcp_ruby/pages/category_detail.yml +24 -0
  750. data/examples/showcase/config/lcp_ruby/pages/chart_showcase.yml +151 -0
  751. data/examples/showcase/config/lcp_ruby/pages/department_detail.yml +55 -0
  752. data/examples/showcase/config/lcp_ruby/pages/department_explorer.yml +60 -0
  753. data/examples/showcase/config/lcp_ruby/pages/employee_overview.yml +106 -0
  754. data/examples/showcase/config/lcp_ruby/pages/employee_transfer_dialog.yml +24 -0
  755. data/examples/showcase/config/lcp_ruby/pages/hr_turnover_dashboard.yml +288 -0
  756. data/examples/showcase/config/lcp_ruby/pages/main_dashboard.yml +142 -0
  757. data/examples/showcase/config/lcp_ruby/pages/monitoring_dashboard.yml +58 -0
  758. data/examples/showcase/config/lcp_ruby/pages/pipeline_detail.yml +16 -0
  759. data/examples/showcase/config/lcp_ruby/pages/showcase_custom_zones.yml +39 -0
  760. data/examples/showcase/config/lcp_ruby/pages/showcase_form_action_dialog.yml +11 -0
  761. data/examples/showcase/config/lcp_ruby/pages/workflow_request_detail.yml +34 -0
  762. data/examples/showcase/config/lcp_ruby/permissions/api_token.yml +26 -0
  763. data/examples/showcase/config/lcp_ruby/permissions/batch_operation.yml +36 -0
  764. data/examples/showcase/config/lcp_ruby/permissions/custom_field_definition.yml +15 -0
  765. data/examples/showcase/config/lcp_ruby/permissions/default.yml +28 -0
  766. data/examples/showcase/config/lcp_ruby/permissions/export_log.yml +23 -0
  767. data/examples/showcase/config/lcp_ruby/permissions/export_profile.yml +25 -0
  768. data/examples/showcase/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
  769. data/examples/showcase/config/lcp_ruby/permissions/group.yml +20 -0
  770. data/examples/showcase/config/lcp_ruby/permissions/group_membership.yml +20 -0
  771. data/examples/showcase/config/lcp_ruby/permissions/group_role_mapping.yml +20 -0
  772. data/examples/showcase/config/lcp_ruby/permissions/host_inventory_item.yml +34 -0
  773. data/examples/showcase/config/lcp_ruby/permissions/import_profile.yml +29 -0
  774. data/examples/showcase/config/lcp_ruby/permissions/import_row.yml +17 -0
  775. data/examples/showcase/config/lcp_ruby/permissions/lcp_error_log.yml +8 -0
  776. data/examples/showcase/config/lcp_ruby/permissions/page_config.yml +15 -0
  777. data/examples/showcase/config/lcp_ruby/permissions/permission_config.yml +15 -0
  778. data/examples/showcase/config/lcp_ruby/permissions/platform_profile.yml +23 -0
  779. data/examples/showcase/config/lcp_ruby/permissions/profile_setting.yml +23 -0
  780. data/examples/showcase/config/lcp_ruby/permissions/profile_tag.yml +23 -0
  781. data/examples/showcase/config/lcp_ruby/permissions/role.yml +20 -0
  782. data/examples/showcase/config/lcp_ruby/permissions/saved_filter.yml +39 -0
  783. data/examples/showcase/config/lcp_ruby/permissions/showcase_announcement.yml +22 -0
  784. data/examples/showcase/config/lcp_ruby/permissions/showcase_audit_log.yml +15 -0
  785. data/examples/showcase/config/lcp_ruby/permissions/showcase_audited_record.yml +15 -0
  786. data/examples/showcase/config/lcp_ruby/permissions/showcase_batch_task.yml +31 -0
  787. data/examples/showcase/config/lcp_ruby/permissions/showcase_condition.yml +54 -0
  788. data/examples/showcase/config/lcp_ruby/permissions/showcase_contact.yml +21 -0
  789. data/examples/showcase/config/lcp_ruby/permissions/showcase_custom_render.yml +21 -0
  790. data/examples/showcase/config/lcp_ruby/permissions/showcase_delete_reason.yml +12 -0
  791. data/examples/showcase/config/lcp_ruby/permissions/showcase_form_action.yml +26 -0
  792. data/examples/showcase/config/lcp_ruby/permissions/showcase_grade.yml +33 -0
  793. data/examples/showcase/config/lcp_ruby/permissions/showcase_job_execution.yml +23 -0
  794. data/examples/showcase/config/lcp_ruby/permissions/showcase_permission.yml +47 -0
  795. data/examples/showcase/config/lcp_ruby/permissions/showcase_quick_note.yml +12 -0
  796. data/examples/showcase/config/lcp_ruby/permissions/showcase_school_class.yml +37 -0
  797. data/examples/showcase/config/lcp_ruby/permissions/showcase_student.yml +43 -0
  798. data/examples/showcase/config/lcp_ruby/permissions/showcase_workflow.yml +34 -0
  799. data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_request.yml +18 -0
  800. data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_step.yml +18 -0
  801. data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_task.yml +18 -0
  802. data/examples/showcase/config/lcp_ruby/permissions/workflow_audit_log.yml +19 -0
  803. data/examples/showcase/config/lcp_ruby/presenters/announcements.rb +59 -0
  804. data/examples/showcase/config/lcp_ruby/presenters/approval_requests.rb +69 -0
  805. data/examples/showcase/config/lcp_ruby/presenters/approval_steps.rb +59 -0
  806. data/examples/showcase/config/lcp_ruby/presenters/approval_tasks.rb +65 -0
  807. data/examples/showcase/config/lcp_ruby/presenters/article_comments_zone.rb +12 -0
  808. data/examples/showcase/config/lcp_ruby/presenters/article_related_zone.rb +15 -0
  809. data/examples/showcase/config/lcp_ruby/presenters/articles.rb +149 -0
  810. data/examples/showcase/config/lcp_ruby/presenters/articles_tiles.rb +30 -0
  811. data/examples/showcase/config/lcp_ruby/presenters/author_articles_zone.rb +12 -0
  812. data/examples/showcase/config/lcp_ruby/presenters/author_show_zone.rb +12 -0
  813. data/examples/showcase/config/lcp_ruby/presenters/authors.rb +39 -0
  814. data/examples/showcase/config/lcp_ruby/presenters/batch_operation_items.yml +45 -0
  815. data/examples/showcase/config/lcp_ruby/presenters/batch_operations.yml +66 -0
  816. data/examples/showcase/config/lcp_ruby/presenters/categories.rb +50 -0
  817. data/examples/showcase/config/lcp_ruby/presenters/category_articles_zone.rb +20 -0
  818. data/examples/showcase/config/lcp_ruby/presenters/category_children_zone.rb +10 -0
  819. data/examples/showcase/config/lcp_ruby/presenters/comment_quick_add_dialog.rb +15 -0
  820. data/examples/showcase/config/lcp_ruby/presenters/custom_fields.rb +124 -0
  821. data/examples/showcase/config/lcp_ruby/presenters/dashboard_employees.rb +16 -0
  822. data/examples/showcase/config/lcp_ruby/presenters/delete_reason_dialog.rb +13 -0
  823. data/examples/showcase/config/lcp_ruby/presenters/departments.rb +55 -0
  824. data/examples/showcase/config/lcp_ruby/presenters/dept_add_employee_dialog.rb +19 -0
  825. data/examples/showcase/config/lcp_ruby/presenters/dept_children_zone.rb +10 -0
  826. data/examples/showcase/config/lcp_ruby/presenters/dept_detail_zone.rb +14 -0
  827. data/examples/showcase/config/lcp_ruby/presenters/dept_employees_zone.rb +16 -0
  828. data/examples/showcase/config/lcp_ruby/presenters/dept_list_selection_zone.rb +19 -0
  829. data/examples/showcase/config/lcp_ruby/presenters/employee_overview_index_zone.rb +36 -0
  830. data/examples/showcase/config/lcp_ruby/presenters/employee_quick_add_dialog.rb +14 -0
  831. data/examples/showcase/config/lcp_ruby/presenters/employee_show_zone.rb +20 -0
  832. data/examples/showcase/config/lcp_ruby/presenters/employee_transfer_form_zone.rb +19 -0
  833. data/examples/showcase/config/lcp_ruby/presenters/employees.rb +165 -0
  834. data/examples/showcase/config/lcp_ruby/presenters/employees_tiles.rb +33 -0
  835. data/examples/showcase/config/lcp_ruby/presenters/export_logs.yml +58 -0
  836. data/examples/showcase/config/lcp_ruby/presenters/export_profiles.yml +70 -0
  837. data/examples/showcase/config/lcp_ruby/presenters/extensibility_quick_edit_dialog.rb +13 -0
  838. data/examples/showcase/config/lcp_ruby/presenters/feature_kanban.rb +30 -0
  839. data/examples/showcase/config/lcp_ruby/presenters/features_card.rb +179 -0
  840. data/examples/showcase/config/lcp_ruby/presenters/features_hub.rb +44 -0
  841. data/examples/showcase/config/lcp_ruby/presenters/features_table.rb +37 -0
  842. data/examples/showcase/config/lcp_ruby/presenters/features_tiles.rb +48 -0
  843. data/examples/showcase/config/lcp_ruby/presenters/group_memberships.rb +54 -0
  844. data/examples/showcase/config/lcp_ruby/presenters/group_role_mappings.rb +48 -0
  845. data/examples/showcase/config/lcp_ruby/presenters/groups.rb +74 -0
  846. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items.rb +99 -0
  847. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_managed.rb +84 -0
  848. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_report.rb +40 -0
  849. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_wizard.rb +76 -0
  850. data/examples/showcase/config/lcp_ruby/presenters/import_profiles.rb +61 -0
  851. data/examples/showcase/config/lcp_ruby/presenters/import_rows.rb +39 -0
  852. data/examples/showcase/config/lcp_ruby/presenters/lcp_error_logs.yml +46 -0
  853. data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens.rb +35 -0
  854. data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens_create_dialog.rb +27 -0
  855. data/examples/showcase/config/lcp_ruby/presenters/my_employee_profile.rb +24 -0
  856. data/examples/showcase/config/lcp_ruby/presenters/my_settings.rb +50 -0
  857. data/examples/showcase/config/lcp_ruby/presenters/page_configs.rb +56 -0
  858. data/examples/showcase/config/lcp_ruby/presenters/permission_configs.rb +58 -0
  859. data/examples/showcase/config/lcp_ruby/presenters/pipeline_edit_zone.rb +11 -0
  860. data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages.rb +51 -0
  861. data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages_zone.rb +11 -0
  862. data/examples/showcase/config/lcp_ruby/presenters/pipelines.rb +40 -0
  863. data/examples/showcase/config/lcp_ruby/presenters/platform_profiles.rb +90 -0
  864. data/examples/showcase/config/lcp_ruby/presenters/projects.rb +58 -0
  865. data/examples/showcase/config/lcp_ruby/presenters/quick_note_dialog.rb +14 -0
  866. data/examples/showcase/config/lcp_ruby/presenters/roles.rb +60 -0
  867. data/examples/showcase/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
  868. data/examples/showcase/config/lcp_ruby/presenters/saved_filters.rb +94 -0
  869. data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregate_items.rb +61 -0
  870. data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates.rb +115 -0
  871. data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates_tiles.rb +44 -0
  872. data/examples/showcase/config/lcp_ruby/presenters/showcase_amount_kanban.rb +31 -0
  873. data/examples/showcase/config/lcp_ruby/presenters/showcase_arrays.rb +140 -0
  874. data/examples/showcase/config/lcp_ruby/presenters/showcase_attachments.rb +66 -0
  875. data/examples/showcase/config/lcp_ruby/presenters/showcase_audit_logs.rb +46 -0
  876. data/examples/showcase/config/lcp_ruby/presenters/showcase_audited_records.rb +58 -0
  877. data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks.rb +97 -0
  878. data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks_archive.rb +52 -0
  879. data/examples/showcase/config/lcp_ruby/presenters/showcase_business_units.rb +43 -0
  880. data/examples/showcase/config/lcp_ruby/presenters/showcase_conditions.rb +195 -0
  881. data/examples/showcase/config/lcp_ruby/presenters/showcase_contacts.rb +82 -0
  882. data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_render_with.rb +50 -0
  883. data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_sections.rb +88 -0
  884. data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_zones_main.rb +75 -0
  885. data/examples/showcase/config/lcp_ruby/presenters/showcase_divisions.rb +53 -0
  886. data/examples/showcase/config/lcp_ruby/presenters/showcase_extensibility.rb +48 -0
  887. data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_card.rb +34 -0
  888. data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_table.rb +128 -0
  889. data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_tiles.rb +42 -0
  890. data/examples/showcase/config/lcp_ruby/presenters/showcase_form_action_dialog.rb +27 -0
  891. data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions.rb +132 -0
  892. data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions_overflow.rb +76 -0
  893. data/examples/showcase/config/lcp_ruby/presenters/showcase_forms.rb +110 -0
  894. data/examples/showcase/config/lcp_ruby/presenters/showcase_grades.rb +38 -0
  895. data/examples/showcase/config/lcp_ruby/presenters/showcase_hr_employees.rb +84 -0
  896. data/examples/showcase/config/lcp_ruby/presenters/showcase_item_classes.rb +120 -0
  897. data/examples/showcase/config/lcp_ruby/presenters/showcase_job_executions.rb +150 -0
  898. data/examples/showcase/config/lcp_ruby/presenters/showcase_memos.rb +96 -0
  899. data/examples/showcase/config/lcp_ruby/presenters/showcase_models.rb +112 -0
  900. data/examples/showcase/config/lcp_ruby/presenters/showcase_organizations.rb +106 -0
  901. data/examples/showcase/config/lcp_ruby/presenters/showcase_people.rb +97 -0
  902. data/examples/showcase/config/lcp_ruby/presenters/showcase_permissions.rb +84 -0
  903. data/examples/showcase/config/lcp_ruby/presenters/showcase_positioning.rb +61 -0
  904. data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes.rb +105 -0
  905. data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes_raw.rb +32 -0
  906. data/examples/showcase/config/lcp_ruby/presenters/showcase_reports.rb +109 -0
  907. data/examples/showcase/config/lcp_ruby/presenters/showcase_school_classes.rb +34 -0
  908. data/examples/showcase/config/lcp_ruby/presenters/showcase_searches.rb +226 -0
  909. data/examples/showcase/config/lcp_ruby/presenters/showcase_sequences.rb +67 -0
  910. data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete.rb +88 -0
  911. data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_archive.rb +52 -0
  912. data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_items.rb +47 -0
  913. data/examples/showcase/config/lcp_ruby/presenters/showcase_students.rb +33 -0
  914. data/examples/showcase/config/lcp_ruby/presenters/showcase_type_defaults.rb +92 -0
  915. data/examples/showcase/config/lcp_ruby/presenters/showcase_userstamps.rb +76 -0
  916. data/examples/showcase/config/lcp_ruby/presenters/showcase_virtual_fields.rb +104 -0
  917. data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_admin.rb +24 -0
  918. data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_kanban.rb +36 -0
  919. data/examples/showcase/config/lcp_ruby/presenters/showcase_workflows.rb +103 -0
  920. data/examples/showcase/config/lcp_ruby/presenters/tags.rb +35 -0
  921. data/examples/showcase/config/lcp_ruby/presenters/users.rb +61 -0
  922. data/examples/showcase/config/lcp_ruby/presenters/weather_stations.rb +60 -0
  923. data/examples/showcase/config/lcp_ruby/presenters/workflow_audit_logs.rb +76 -0
  924. data/examples/showcase/config/lcp_ruby/presenters/workflow_request_audit_zone.rb +28 -0
  925. data/examples/showcase/config/lcp_ruby/theme.yml +2 -0
  926. data/examples/showcase/config/lcp_ruby/types/currency.yml +15 -0
  927. data/examples/showcase/config/lcp_ruby/types/percentage.yml +16 -0
  928. data/examples/showcase/config/lcp_ruby/types/rating.yml +20 -0
  929. data/examples/showcase/config/lcp_ruby/views/announcements.yml +7 -0
  930. data/examples/showcase/config/lcp_ruby/views/approval_requests.yml +7 -0
  931. data/examples/showcase/config/lcp_ruby/views/approval_steps.yml +7 -0
  932. data/examples/showcase/config/lcp_ruby/views/approval_tasks.yml +7 -0
  933. data/examples/showcase/config/lcp_ruby/views/articles.yml +12 -0
  934. data/examples/showcase/config/lcp_ruby/views/authors.yml +7 -0
  935. data/examples/showcase/config/lcp_ruby/views/batch_operation_items.yml +6 -0
  936. data/examples/showcase/config/lcp_ruby/views/batch_operations.yml +10 -0
  937. data/examples/showcase/config/lcp_ruby/views/categories.yml +9 -0
  938. data/examples/showcase/config/lcp_ruby/views/custom_fields.rb +8 -0
  939. data/examples/showcase/config/lcp_ruby/views/dashboard.yml +10 -0
  940. data/examples/showcase/config/lcp_ruby/views/department_explorer.yml +7 -0
  941. data/examples/showcase/config/lcp_ruby/views/departments.yml +9 -0
  942. data/examples/showcase/config/lcp_ruby/views/employee_overview.yml +7 -0
  943. data/examples/showcase/config/lcp_ruby/views/employees.yml +12 -0
  944. data/examples/showcase/config/lcp_ruby/views/export_logs.yml +7 -0
  945. data/examples/showcase/config/lcp_ruby/views/export_profiles.yml +7 -0
  946. data/examples/showcase/config/lcp_ruby/views/features.yml +19 -0
  947. data/examples/showcase/config/lcp_ruby/views/group_memberships.yml +7 -0
  948. data/examples/showcase/config/lcp_ruby/views/group_role_mappings.yml +7 -0
  949. data/examples/showcase/config/lcp_ruby/views/groups.yml +7 -0
  950. data/examples/showcase/config/lcp_ruby/views/host_inventory_items.yml +16 -0
  951. data/examples/showcase/config/lcp_ruby/views/hr_turnover_dashboard.yml +7 -0
  952. data/examples/showcase/config/lcp_ruby/views/import_profiles.yml +7 -0
  953. data/examples/showcase/config/lcp_ruby/views/import_rows.yml +7 -0
  954. data/examples/showcase/config/lcp_ruby/views/lcp_error_logs.yml +10 -0
  955. data/examples/showcase/config/lcp_ruby/views/monitoring.yml +7 -0
  956. data/examples/showcase/config/lcp_ruby/views/my_api_tokens.rb +9 -0
  957. data/examples/showcase/config/lcp_ruby/views/page_configs.yml +11 -0
  958. data/examples/showcase/config/lcp_ruby/views/permission_configs.yml +11 -0
  959. data/examples/showcase/config/lcp_ruby/views/pipeline_stages.yml +7 -0
  960. data/examples/showcase/config/lcp_ruby/views/pipelines.yml +7 -0
  961. data/examples/showcase/config/lcp_ruby/views/platform_profiles.yml +7 -0
  962. data/examples/showcase/config/lcp_ruby/views/projects.yml +9 -0
  963. data/examples/showcase/config/lcp_ruby/views/roles.yml +7 -0
  964. data/examples/showcase/config/lcp_ruby/views/saved_filters.yml +7 -0
  965. data/examples/showcase/config/lcp_ruby/views/showcase_aggregate_items.yml +7 -0
  966. data/examples/showcase/config/lcp_ruby/views/showcase_aggregates.yml +10 -0
  967. data/examples/showcase/config/lcp_ruby/views/showcase_arrays.yml +7 -0
  968. data/examples/showcase/config/lcp_ruby/views/showcase_attachments.yml +7 -0
  969. data/examples/showcase/config/lcp_ruby/views/showcase_audit_logs.yml +11 -0
  970. data/examples/showcase/config/lcp_ruby/views/showcase_audited_records.yml +10 -0
  971. data/examples/showcase/config/lcp_ruby/views/showcase_batch_tasks.yml +10 -0
  972. data/examples/showcase/config/lcp_ruby/views/showcase_business_units.yml +7 -0
  973. data/examples/showcase/config/lcp_ruby/views/showcase_conditions.yml +7 -0
  974. data/examples/showcase/config/lcp_ruby/views/showcase_contacts.yml +7 -0
  975. data/examples/showcase/config/lcp_ruby/views/showcase_custom_render_with.yml +7 -0
  976. data/examples/showcase/config/lcp_ruby/views/showcase_custom_sections.yml +7 -0
  977. data/examples/showcase/config/lcp_ruby/views/showcase_custom_zones.yml +7 -0
  978. data/examples/showcase/config/lcp_ruby/views/showcase_divisions.yml +9 -0
  979. data/examples/showcase/config/lcp_ruby/views/showcase_extensibility.yml +7 -0
  980. data/examples/showcase/config/lcp_ruby/views/showcase_fields.yml +13 -0
  981. data/examples/showcase/config/lcp_ruby/views/showcase_form_actions.yml +10 -0
  982. data/examples/showcase/config/lcp_ruby/views/showcase_forms.yml +7 -0
  983. data/examples/showcase/config/lcp_ruby/views/showcase_grades.yml +9 -0
  984. data/examples/showcase/config/lcp_ruby/views/showcase_hr_employees.yml +9 -0
  985. data/examples/showcase/config/lcp_ruby/views/showcase_item_classes.yml +7 -0
  986. data/examples/showcase/config/lcp_ruby/views/showcase_job_executions.yml +9 -0
  987. data/examples/showcase/config/lcp_ruby/views/showcase_memos.yml +7 -0
  988. data/examples/showcase/config/lcp_ruby/views/showcase_models.yml +7 -0
  989. data/examples/showcase/config/lcp_ruby/views/showcase_organizations.yml +7 -0
  990. data/examples/showcase/config/lcp_ruby/views/showcase_people.yml +7 -0
  991. data/examples/showcase/config/lcp_ruby/views/showcase_permissions.yml +7 -0
  992. data/examples/showcase/config/lcp_ruby/views/showcase_positioning.yml +7 -0
  993. data/examples/showcase/config/lcp_ruby/views/showcase_recipes.yml +10 -0
  994. data/examples/showcase/config/lcp_ruby/views/showcase_reports.yml +7 -0
  995. data/examples/showcase/config/lcp_ruby/views/showcase_school_classes.yml +7 -0
  996. data/examples/showcase/config/lcp_ruby/views/showcase_searches.yml +7 -0
  997. data/examples/showcase/config/lcp_ruby/views/showcase_sequences.yml +7 -0
  998. data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete.yml +10 -0
  999. data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete_items.yml +8 -0
  1000. data/examples/showcase/config/lcp_ruby/views/showcase_students.yml +9 -0
  1001. data/examples/showcase/config/lcp_ruby/views/showcase_userstamps.yml +7 -0
  1002. data/examples/showcase/config/lcp_ruby/views/showcase_virtual_fields.yml +7 -0
  1003. data/examples/showcase/config/lcp_ruby/views/showcase_workflows.yml +16 -0
  1004. data/examples/showcase/config/lcp_ruby/views/tags.yml +7 -0
  1005. data/examples/showcase/config/lcp_ruby/views/users.yml +7 -0
  1006. data/examples/showcase/config/lcp_ruby/views/weather_stations.yml +7 -0
  1007. data/examples/showcase/config/lcp_ruby/views/workflow_audit_logs.yml +9 -0
  1008. data/examples/showcase/config/lcp_ruby/workflows/showcase_form_action.rb +48 -0
  1009. data/examples/showcase/config/lcp_ruby/workflows/showcase_workflow.rb +199 -0
  1010. data/examples/showcase/config/locales/cs.yml +2818 -0
  1011. data/examples/showcase/config/locales/en.yml +67 -0
  1012. data/examples/showcase/config/locales/lcp_ruby/api_tokens.cs.yml +46 -0
  1013. data/examples/showcase/config/locales/lcp_ruby/api_tokens.en.yml +49 -0
  1014. data/examples/showcase/config/routes.rb +21 -0
  1015. data/examples/showcase/config/storage.yml +3 -0
  1016. data/examples/showcase/config.ru +2 -0
  1017. data/examples/showcase/db/migrate/20260219124321_create_active_storage_tables.active_storage.rb +57 -0
  1018. data/examples/showcase/db/migrate/20260220072723_create_lcp_ruby_users.rb +42 -0
  1019. data/examples/showcase/db/migrate/20260406120000_create_host_inventory_items.rb +32 -0
  1020. data/examples/showcase/db/migrate/20260428134542_add_oidc_columns_to_lcp_ruby_users.rb +13 -0
  1021. data/examples/showcase/db/migrate/20260502120000_create_platform_profiles.rb +19 -0
  1022. data/examples/showcase/db/schema.rb +1222 -0
  1023. data/examples/showcase/db/seeds.rb +3085 -0
  1024. data/examples/showcase/erd.md +567 -0
  1025. data/examples/showcase/test/fixtures/import_articles.csv +4 -0
  1026. data/examples/showcase/test/fixtures/import_employees.csv +4 -0
  1027. data/examples/todo/Gemfile +8 -0
  1028. data/examples/todo/Gemfile.lock +415 -0
  1029. data/examples/todo/Rakefile +2 -0
  1030. data/examples/todo/app/assets/config/manifest.js +1 -0
  1031. data/examples/todo/app/controllers/application_controller.rb +6 -0
  1032. data/examples/todo/app/lcp_services/defaults/one_week_from_now.rb +11 -0
  1033. data/examples/todo/bin/rails +4 -0
  1034. data/examples/todo/bin/rake +4 -0
  1035. data/examples/todo/config/application.rb +31 -0
  1036. data/examples/todo/config/boot.rb +2 -0
  1037. data/examples/todo/config/database.yml +12 -0
  1038. data/examples/todo/config/environment.rb +2 -0
  1039. data/examples/todo/config/lcp_ruby/models/todo_item.yml +67 -0
  1040. data/examples/todo/config/lcp_ruby/models/todo_list.yml +49 -0
  1041. data/examples/todo/config/lcp_ruby/permissions/default.yml +10 -0
  1042. data/examples/todo/config/lcp_ruby/permissions/todo_item.yml +21 -0
  1043. data/examples/todo/config/lcp_ruby/presenters/todo_item.yml +75 -0
  1044. data/examples/todo/config/lcp_ruby/presenters/todo_list.yml +68 -0
  1045. data/examples/todo/config/lcp_ruby/views/todo_items.yml +11 -0
  1046. data/examples/todo/config/lcp_ruby/views/todo_lists.yml +9 -0
  1047. data/examples/todo/config/routes.rb +4 -0
  1048. data/examples/todo/config/storage.yml +3 -0
  1049. data/examples/todo/config.ru +2 -0
  1050. data/examples/todo/db/migrate/20260219103416_create_active_storage_tables.active_storage.rb +57 -0
  1051. data/examples/todo/db/schema.rb +63 -0
  1052. data/examples/todo/db/seeds.rb +24 -0
  1053. data/examples/todo/erd.md +21 -0
  1054. data/exe/lcp +33 -0
  1055. data/lib/generators/lcp_ruby/agent_setup_generator.rb +102 -0
  1056. data/lib/generators/lcp_ruby/api_tokens_generator.rb +102 -0
  1057. data/lib/generators/lcp_ruby/auditing_generator.rb +54 -0
  1058. data/lib/generators/lcp_ruby/background_jobs_generator.rb +61 -0
  1059. data/lib/generators/lcp_ruby/batch_operations_generator.rb +62 -0
  1060. data/lib/generators/lcp_ruby/claude_skills_generator.rb +47 -0
  1061. data/lib/generators/lcp_ruby/custom_fields_generator.rb +54 -0
  1062. data/lib/generators/lcp_ruby/dsl_to_yaml.rb +72 -0
  1063. data/lib/generators/lcp_ruby/entity/color_palette.rb +22 -0
  1064. data/lib/generators/lcp_ruby/entity/field_descriptor.rb +24 -0
  1065. data/lib/generators/lcp_ruby/entity/field_token_parser.rb +101 -0
  1066. data/lib/generators/lcp_ruby/entity/role_discovery.rb +45 -0
  1067. data/lib/generators/lcp_ruby/entity_generator.rb +1104 -0
  1068. data/lib/generators/lcp_ruby/export_generator.rb +71 -0
  1069. data/lib/generators/lcp_ruby/format_support.rb +56 -0
  1070. data/lib/generators/lcp_ruby/gapfree_sequences_generator.rb +64 -0
  1071. data/lib/generators/lcp_ruby/groups_generator.rb +94 -0
  1072. data/lib/generators/lcp_ruby/host_controller_generator.rb +202 -0
  1073. data/lib/generators/lcp_ruby/import_generator.rb +96 -0
  1074. data/lib/generators/lcp_ruby/install_auth_generator.rb +432 -0
  1075. data/lib/generators/lcp_ruby/install_generator.rb +319 -0
  1076. data/lib/generators/lcp_ruby/monitoring_generator.rb +58 -0
  1077. data/lib/generators/lcp_ruby/oidc_role_mappings_generator.rb +60 -0
  1078. data/lib/generators/lcp_ruby/pages_generator.rb +66 -0
  1079. data/lib/generators/lcp_ruby/permission_source_generator.rb +66 -0
  1080. data/lib/generators/lcp_ruby/role_model_generator.rb +73 -0
  1081. data/lib/generators/lcp_ruby/saved_filters_generator.rb +62 -0
  1082. data/lib/generators/lcp_ruby/templates/add_oidc_columns_to_lcp_ruby_users.rb.erb +13 -0
  1083. data/lib/generators/lcp_ruby/templates/agent_setup/agents_md.md +3 -0
  1084. data/lib/generators/lcp_ruby/templates/agent_setup/claude_md.md +12 -0
  1085. data/lib/generators/lcp_ruby/templates/api_tokens/create_dialog.rb +31 -0
  1086. data/lib/generators/lcp_ruby/templates/api_tokens/locales.en.yml +43 -0
  1087. data/lib/generators/lcp_ruby/templates/api_tokens/model.rb +23 -0
  1088. data/lib/generators/lcp_ruby/templates/api_tokens/permissions.yml +30 -0
  1089. data/lib/generators/lcp_ruby/templates/api_tokens/presenter.rb +38 -0
  1090. data/lib/generators/lcp_ruby/templates/api_tokens/view_group.rb +13 -0
  1091. data/lib/generators/lcp_ruby/templates/auditing/model.rb +34 -0
  1092. data/lib/generators/lcp_ruby/templates/auditing/permissions.yml +19 -0
  1093. data/lib/generators/lcp_ruby/templates/auditing/presenter.rb +41 -0
  1094. data/lib/generators/lcp_ruby/templates/auditing/view_group.rb +10 -0
  1095. data/lib/generators/lcp_ruby/templates/background_jobs/model.rb +59 -0
  1096. data/lib/generators/lcp_ruby/templates/background_jobs/permissions.yml +22 -0
  1097. data/lib/generators/lcp_ruby/templates/background_jobs/presenter.rb +82 -0
  1098. data/lib/generators/lcp_ruby/templates/background_jobs/view_group.rb +9 -0
  1099. data/lib/generators/lcp_ruby/templates/batch_operations/item_model.rb +28 -0
  1100. data/lib/generators/lcp_ruby/templates/batch_operations/item_presenter.rb +43 -0
  1101. data/lib/generators/lcp_ruby/templates/batch_operations/model.rb +42 -0
  1102. data/lib/generators/lcp_ruby/templates/batch_operations/permissions.yml +44 -0
  1103. data/lib/generators/lcp_ruby/templates/batch_operations/presenter.rb +61 -0
  1104. data/lib/generators/lcp_ruby/templates/batch_operations/view_group.rb +9 -0
  1105. data/lib/generators/lcp_ruby/templates/create_lcp_ruby_users.rb.erb +50 -0
  1106. data/lib/generators/lcp_ruby/templates/custom_fields/model.rb +60 -0
  1107. data/lib/generators/lcp_ruby/templates/custom_fields/permissions.yml +19 -0
  1108. data/lib/generators/lcp_ruby/templates/custom_fields/presenter.rb +128 -0
  1109. data/lib/generators/lcp_ruby/templates/custom_fields/view_group.rb +9 -0
  1110. data/lib/generators/lcp_ruby/templates/entity/model.rb +42 -0
  1111. data/lib/generators/lcp_ruby/templates/entity/permissions.yml +20 -0
  1112. data/lib/generators/lcp_ruby/templates/entity/presenter.rb +55 -0
  1113. data/lib/generators/lcp_ruby/templates/entity/view_group.rb +8 -0
  1114. data/lib/generators/lcp_ruby/templates/export/export_log_model.rb +30 -0
  1115. data/lib/generators/lcp_ruby/templates/export/export_log_permissions.yml +24 -0
  1116. data/lib/generators/lcp_ruby/templates/export/export_logs_presenter.rb +51 -0
  1117. data/lib/generators/lcp_ruby/templates/export/export_profile_model.rb +28 -0
  1118. data/lib/generators/lcp_ruby/templates/export/export_profile_permissions.yml +26 -0
  1119. data/lib/generators/lcp_ruby/templates/export/export_profiles_presenter.rb +59 -0
  1120. data/lib/generators/lcp_ruby/templates/gapfree_sequences/model.rb +18 -0
  1121. data/lib/generators/lcp_ruby/templates/gapfree_sequences/permissions.yml +19 -0
  1122. data/lib/generators/lcp_ruby/templates/gapfree_sequences/presenter.rb +51 -0
  1123. data/lib/generators/lcp_ruby/templates/gapfree_sequences/view_group.rb +9 -0
  1124. data/lib/generators/lcp_ruby/templates/groups/group_membership_model.rb +17 -0
  1125. data/lib/generators/lcp_ruby/templates/groups/group_model.rb +28 -0
  1126. data/lib/generators/lcp_ruby/templates/groups/group_permissions.yml +19 -0
  1127. data/lib/generators/lcp_ruby/templates/groups/group_presenter.rb +53 -0
  1128. data/lib/generators/lcp_ruby/templates/groups/group_role_mapping_model.rb +15 -0
  1129. data/lib/generators/lcp_ruby/templates/groups/group_view_group.rb +9 -0
  1130. data/lib/generators/lcp_ruby/templates/host_controller/controller.rb.erb +131 -0
  1131. data/lib/generators/lcp_ruby/templates/import/data_import_job.yml +7 -0
  1132. data/lib/generators/lcp_ruby/templates/import/import_profile_model.rb +30 -0
  1133. data/lib/generators/lcp_ruby/templates/import/import_profile_permissions.yml +29 -0
  1134. data/lib/generators/lcp_ruby/templates/import/import_profiles_presenter.rb +57 -0
  1135. data/lib/generators/lcp_ruby/templates/import/import_row_model.rb +32 -0
  1136. data/lib/generators/lcp_ruby/templates/import/import_row_permissions.yml +11 -0
  1137. data/lib/generators/lcp_ruby/templates/import/import_rows_presenter.rb +35 -0
  1138. data/lib/generators/lcp_ruby/templates/install/default_permissions.yml +26 -0
  1139. data/lib/generators/lcp_ruby/templates/install/menu.yml.tt +71 -0
  1140. data/lib/generators/lcp_ruby/templates/install/model.rb +20 -0
  1141. data/lib/generators/lcp_ruby/templates/install/permissions.yml +25 -0
  1142. data/lib/generators/lcp_ruby/templates/install/presenter.rb +44 -0
  1143. data/lib/generators/lcp_ruby/templates/install/view_group.rb +10 -0
  1144. data/lib/generators/lcp_ruby/templates/install_auth/oidc/entra.yml.erb +46 -0
  1145. data/lib/generators/lcp_ruby/templates/install_auth/oidc/generic.yml.erb +55 -0
  1146. data/lib/generators/lcp_ruby/templates/install_auth/oidc/google.yml.erb +42 -0
  1147. data/lib/generators/lcp_ruby/templates/install_auth/oidc/keycloak.yml.erb +45 -0
  1148. data/lib/generators/lcp_ruby/templates/install_auth/oidc/okta.yml.erb +45 -0
  1149. data/lib/generators/lcp_ruby/templates/install_auth/user.rb +36 -0
  1150. data/lib/generators/lcp_ruby/templates/monitoring/model.rb +34 -0
  1151. data/lib/generators/lcp_ruby/templates/monitoring/model_extension.rb +12 -0
  1152. data/lib/generators/lcp_ruby/templates/monitoring/page.yml +49 -0
  1153. data/lib/generators/lcp_ruby/templates/monitoring/permissions.yml +8 -0
  1154. data/lib/generators/lcp_ruby/templates/monitoring/presenter.rb +44 -0
  1155. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/locales.en.yml +15 -0
  1156. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/model.rb +32 -0
  1157. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/permissions.yml +21 -0
  1158. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/presenter.rb +41 -0
  1159. data/lib/generators/lcp_ruby/templates/pages/model.rb +19 -0
  1160. data/lib/generators/lcp_ruby/templates/pages/permissions.yml +19 -0
  1161. data/lib/generators/lcp_ruby/templates/pages/presenter.rb +51 -0
  1162. data/lib/generators/lcp_ruby/templates/pages/view_group.rb +10 -0
  1163. data/lib/generators/lcp_ruby/templates/permission_source/model.rb +21 -0
  1164. data/lib/generators/lcp_ruby/templates/permission_source/permissions.yml +19 -0
  1165. data/lib/generators/lcp_ruby/templates/permission_source/presenter.rb +51 -0
  1166. data/lib/generators/lcp_ruby/templates/permission_source/view_group.rb +10 -0
  1167. data/lib/generators/lcp_ruby/templates/role_model/model.rb +24 -0
  1168. data/lib/generators/lcp_ruby/templates/role_model/permissions.yml +19 -0
  1169. data/lib/generators/lcp_ruby/templates/role_model/presenter.rb +62 -0
  1170. data/lib/generators/lcp_ruby/templates/role_model/view_group.rb +10 -0
  1171. data/lib/generators/lcp_ruby/templates/saved_filters/model.rb +51 -0
  1172. data/lib/generators/lcp_ruby/templates/saved_filters/permissions.yml +44 -0
  1173. data/lib/generators/lcp_ruby/templates/saved_filters/presenter.rb +96 -0
  1174. data/lib/generators/lcp_ruby/templates/saved_filters/save_dialog_presenter.rb +19 -0
  1175. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_model.rb +50 -0
  1176. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_permissions.yml +10 -0
  1177. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_presenter.rb +44 -0
  1178. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_view_group.rb +10 -0
  1179. data/lib/generators/lcp_ruby/templates/workflow_approvals/step_model.rb +36 -0
  1180. data/lib/generators/lcp_ruby/templates/workflow_approvals/step_permissions.yml +10 -0
  1181. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_model.rb +34 -0
  1182. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_permissions.yml +10 -0
  1183. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_presenter.rb +39 -0
  1184. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_view_group.rb +10 -0
  1185. data/lib/generators/lcp_ruby/templates/workflow_audit_log/model.rb +48 -0
  1186. data/lib/generators/lcp_ruby/templates/workflow_audit_log/permissions.yml +10 -0
  1187. data/lib/generators/lcp_ruby/templates/workflow_audit_log/presenter.rb +44 -0
  1188. data/lib/generators/lcp_ruby/templates/workflow_audit_log/view_group.rb +10 -0
  1189. data/lib/generators/lcp_ruby/templates/workflow_definition/model.rb +43 -0
  1190. data/lib/generators/lcp_ruby/templates/workflow_definition/permissions.yml +19 -0
  1191. data/lib/generators/lcp_ruby/templates/workflow_definition/presenter.rb +70 -0
  1192. data/lib/generators/lcp_ruby/templates/workflow_definition/view_group.rb +10 -0
  1193. data/lib/generators/lcp_ruby/workflow_approvals_generator.rb +93 -0
  1194. data/lib/generators/lcp_ruby/workflow_audit_log_generator.rb +54 -0
  1195. data/lib/generators/lcp_ruby/workflow_definition_generator.rb +77 -0
  1196. data/lib/lcp.rb +6 -0
  1197. data/lib/lcp_ruby/actions/action_executor.rb +66 -0
  1198. data/lib/lcp_ruby/actions/action_registry.rb +48 -0
  1199. data/lib/lcp_ruby/actions/api_tokens/revoke.rb +13 -0
  1200. data/lib/lcp_ruby/actions/base_action.rb +79 -0
  1201. data/lib/lcp_ruby/actions/form_action_pipeline.rb +138 -0
  1202. data/lib/lcp_ruby/aggregates/query_builder.rb +6 -0
  1203. data/lib/lcp_ruby/api_tokens/model_extension.rb +41 -0
  1204. data/lib/lcp_ruby/api_tokens/resolver_registry.rb +53 -0
  1205. data/lib/lcp_ruby/api_tokens/token_generator.rb +27 -0
  1206. data/lib/lcp_ruby/api_tokens/verifier.rb +38 -0
  1207. data/lib/lcp_ruby/app_template.rb +181 -0
  1208. data/lib/lcp_ruby/array_query.rb +120 -0
  1209. data/lib/lcp_ruby/asset_copier.rb +62 -0
  1210. data/lib/lcp_ruby/association_fk_type.rb +191 -0
  1211. data/lib/lcp_ruby/association_join_column.rb +28 -0
  1212. data/lib/lcp_ruby/association_options_builder.rb +231 -0
  1213. data/lib/lcp_ruby/auditing/audit_writer.rb +258 -0
  1214. data/lib/lcp_ruby/auditing/contract_validator.rb +95 -0
  1215. data/lib/lcp_ruby/auditing/registry.rb +29 -0
  1216. data/lib/lcp_ruby/auditing/setup.rb +49 -0
  1217. data/lib/lcp_ruby/authentication/audit_subscriber.rb +51 -0
  1218. data/lib/lcp_ruby/authentication/bearer_jwt_verifier.rb +139 -0
  1219. data/lib/lcp_ruby/authentication/devise_setup.rb +47 -0
  1220. data/lib/lcp_ruby/authentication/errors.rb +27 -0
  1221. data/lib/lcp_ruby/authentication/http_fetcher.rb +36 -0
  1222. data/lib/lcp_ruby/authentication/jwks_cache.rb +91 -0
  1223. data/lib/lcp_ruby/authentication/oidc_bearer_resolver.rb +84 -0
  1224. data/lib/lcp_ruby/authentication/omniauth_builder.rb +147 -0
  1225. data/lib/lcp_ruby/authentication/provider.rb +108 -0
  1226. data/lib/lcp_ruby/authentication/provider_registry.rb +227 -0
  1227. data/lib/lcp_ruby/authentication/role_mapper.rb +94 -0
  1228. data/lib/lcp_ruby/authentication/test_support.rb +257 -0
  1229. data/lib/lcp_ruby/authentication/user_resolver.rb +169 -0
  1230. data/lib/lcp_ruby/authentication.rb +40 -0
  1231. data/lib/lcp_ruby/authorization/association_lookup.rb +56 -0
  1232. data/lib/lcp_ruby/authorization/authorization_error.rb +12 -0
  1233. data/lib/lcp_ruby/authorization/cache.rb +89 -0
  1234. data/lib/lcp_ruby/authorization/codes.rb +17 -0
  1235. data/lib/lcp_ruby/authorization/impersonated_user.rb +29 -0
  1236. data/lib/lcp_ruby/authorization/includes_hint.rb +110 -0
  1237. data/lib/lcp_ruby/authorization/inherited_parent_validator.rb +142 -0
  1238. data/lib/lcp_ruby/authorization/invariant_check/configuration.rb +132 -0
  1239. data/lib/lcp_ruby/authorization/invariant_error.rb +15 -0
  1240. data/lib/lcp_ruby/authorization/misconfigured_page_error.rb +30 -0
  1241. data/lib/lcp_ruby/authorization/page_gate.rb +57 -0
  1242. data/lib/lcp_ruby/authorization/permission_evaluator.rb +343 -0
  1243. data/lib/lcp_ruby/authorization/policy_factory.rb +91 -0
  1244. data/lib/lcp_ruby/authorization/runtime_invariant_validator.rb +421 -0
  1245. data/lib/lcp_ruby/authorization/scope_builder.rb +227 -0
  1246. data/lib/lcp_ruby/authorization/scope_resolver.rb +28 -0
  1247. data/lib/lcp_ruby/authorized_controller.rb +44 -0
  1248. data/lib/lcp_ruby/background_jobs/base_handler.rb +113 -0
  1249. data/lib/lcp_ruby/background_jobs/change_handler.rb +17 -0
  1250. data/lib/lcp_ruby/background_jobs/contract.rb +16 -0
  1251. data/lib/lcp_ruby/background_jobs/contract_validator.rb +112 -0
  1252. data/lib/lcp_ruby/background_jobs/declarative/base_action.rb +11 -0
  1253. data/lib/lcp_ruby/background_jobs/declarative/call_webhook_action.rb +174 -0
  1254. data/lib/lcp_ruby/background_jobs/declarative/fire_event_action.rb +24 -0
  1255. data/lib/lcp_ruby/background_jobs/declarative/registry.rb +34 -0
  1256. data/lib/lcp_ruby/background_jobs/declarative/run_scope_action.rb +98 -0
  1257. data/lib/lcp_ruby/background_jobs/declarative/send_notification_action.rb +13 -0
  1258. data/lib/lcp_ruby/background_jobs/definition.rb +134 -0
  1259. data/lib/lcp_ruby/background_jobs/enqueue.rb +83 -0
  1260. data/lib/lcp_ruby/background_jobs/errors.rb +9 -0
  1261. data/lib/lcp_ruby/background_jobs/executor_job.rb +111 -0
  1262. data/lib/lcp_ruby/background_jobs/handler_factory.rb +46 -0
  1263. data/lib/lcp_ruby/background_jobs/host_source.rb +33 -0
  1264. data/lib/lcp_ruby/background_jobs/model_source.rb +93 -0
  1265. data/lib/lcp_ruby/background_jobs/registry.rb +81 -0
  1266. data/lib/lcp_ruby/background_jobs/resolver.rb +29 -0
  1267. data/lib/lcp_ruby/background_jobs/schedule_adapter.rb +14 -0
  1268. data/lib/lcp_ruby/background_jobs/setup.rb +145 -0
  1269. data/lib/lcp_ruby/background_jobs/static_source.rb +19 -0
  1270. data/lib/lcp_ruby/background_jobs/steps_executor.rb +52 -0
  1271. data/lib/lcp_ruby/background_jobs/triggers/event_trigger.rb +97 -0
  1272. data/lib/lcp_ruby/background_jobs/triggers/trigger_installer.rb +20 -0
  1273. data/lib/lcp_ruby/background_jobs/unique_key_builder.rb +31 -0
  1274. data/lib/lcp_ruby/batch_actions/base_service.rb +70 -0
  1275. data/lib/lcp_ruby/batch_actions/batch_action_handler.rb +200 -0
  1276. data/lib/lcp_ruby/batch_actions/custom_action_dispatcher.rb +133 -0
  1277. data/lib/lcp_ruby/batch_actions/destroy_service.rb +37 -0
  1278. data/lib/lcp_ruby/batch_actions/permanently_destroy_service.rb +33 -0
  1279. data/lib/lcp_ruby/batch_actions/restore_service.rb +33 -0
  1280. data/lib/lcp_ruby/batch_actions.rb +5 -0
  1281. data/lib/lcp_ruby/bulk_updater.rb +25 -0
  1282. data/lib/lcp_ruby/cli/docs_command.rb +29 -0
  1283. data/lib/lcp_ruby/cli/examples_command.rb +29 -0
  1284. data/lib/lcp_ruby/cli/new_command.rb +509 -0
  1285. data/lib/lcp_ruby/cli/run_command.rb +155 -0
  1286. data/lib/lcp_ruby/cli/skills_command.rb +54 -0
  1287. data/lib/lcp_ruby/cli.rb +74 -0
  1288. data/lib/lcp_ruby/condition_evaluator.rb +366 -0
  1289. data/lib/lcp_ruby/condition_service_registry.rb +58 -0
  1290. data/lib/lcp_ruby/condition_services/current_user_role.rb +28 -0
  1291. data/lib/lcp_ruby/condition_services/feature_flag.rb +63 -0
  1292. data/lib/lcp_ruby/condition_services/impersonating.rb +24 -0
  1293. data/lib/lcp_ruby/conditions/validator.rb +35 -0
  1294. data/lib/lcp_ruby/configuration.rb +431 -0
  1295. data/lib/lcp_ruby/controller/authentication.rb +118 -0
  1296. data/lib/lcp_ruby/controller/authorization.rb +198 -0
  1297. data/lib/lcp_ruby/controller/bearer_authentication.rb +76 -0
  1298. data/lib/lcp_ruby/controller/crud_helpers.rb +233 -0
  1299. data/lib/lcp_ruby/controller/error_handling.rb +94 -0
  1300. data/lib/lcp_ruby/controller/impersonation.rb +70 -0
  1301. data/lib/lcp_ruby/controller/locale_binding.rb +62 -0
  1302. data/lib/lcp_ruby/controller/path_helpers.rb +125 -0
  1303. data/lib/lcp_ruby/controller/presenter_setup.rb +89 -0
  1304. data/lib/lcp_ruby/controller/search.rb +321 -0
  1305. data/lib/lcp_ruby/controller/view_helpers.rb +105 -0
  1306. data/lib/lcp_ruby/current.rb +13 -0
  1307. data/lib/lcp_ruby/custom_fields/applicator.rb +194 -0
  1308. data/lib/lcp_ruby/custom_fields/contract_validator.rb +77 -0
  1309. data/lib/lcp_ruby/custom_fields/definition_change_handler.rb +21 -0
  1310. data/lib/lcp_ruby/custom_fields/query.rb +112 -0
  1311. data/lib/lcp_ruby/custom_fields/registry.rb +70 -0
  1312. data/lib/lcp_ruby/custom_fields/setup.rb +58 -0
  1313. data/lib/lcp_ruby/custom_fields/utils.rb +40 -0
  1314. data/lib/lcp_ruby/custom_fields.rb +9 -0
  1315. data/lib/lcp_ruby/data_source/api_error_placeholder.rb +47 -0
  1316. data/lib/lcp_ruby/data_source/api_filter_translator.rb +72 -0
  1317. data/lib/lcp_ruby/data_source/api_model_concern.rb +131 -0
  1318. data/lib/lcp_ruby/data_source/api_preloader.rb +44 -0
  1319. data/lib/lcp_ruby/data_source/base.rb +85 -0
  1320. data/lib/lcp_ruby/data_source/cached_wrapper.rb +107 -0
  1321. data/lib/lcp_ruby/data_source/host.rb +71 -0
  1322. data/lib/lcp_ruby/data_source/registry.rb +39 -0
  1323. data/lib/lcp_ruby/data_source/resilient_wrapper.rb +67 -0
  1324. data/lib/lcp_ruby/data_source/rest_json.rb +247 -0
  1325. data/lib/lcp_ruby/data_source/setup.rb +57 -0
  1326. data/lib/lcp_ruby/dev_toolbar.rb +8 -0
  1327. data/lib/lcp_ruby/display/base_renderer.rb +21 -0
  1328. data/lib/lcp_ruby/display/count_badge.rb +11 -0
  1329. data/lib/lcp_ruby/display/icon_badge.rb +17 -0
  1330. data/lib/lcp_ruby/display/renderer_registry.rb +138 -0
  1331. data/lib/lcp_ruby/display/renderers/attachment_link.rb +26 -0
  1332. data/lib/lcp_ruby/display/renderers/attachment_list.rb +32 -0
  1333. data/lib/lcp_ruby/display/renderers/attachment_preview.rb +26 -0
  1334. data/lib/lcp_ruby/display/renderers/avatar.rb +14 -0
  1335. data/lib/lcp_ruby/display/renderers/badge.rb +43 -0
  1336. data/lib/lcp_ruby/display/renderers/boolean_icon.rb +54 -0
  1337. data/lib/lcp_ruby/display/renderers/code.rb +39 -0
  1338. data/lib/lcp_ruby/display/renderers/collection.rb +34 -0
  1339. data/lib/lcp_ruby/display/renderers/color_swatch.rb +25 -0
  1340. data/lib/lcp_ruby/display/renderers/concerns/attachment_helpers.rb +73 -0
  1341. data/lib/lcp_ruby/display/renderers/concerns/workflow_helpers.rb +35 -0
  1342. data/lib/lcp_ruby/display/renderers/copy_code.rb +33 -0
  1343. data/lib/lcp_ruby/display/renderers/currency.rb +14 -0
  1344. data/lib/lcp_ruby/display/renderers/date.rb +17 -0
  1345. data/lib/lcp_ruby/display/renderers/datetime.rb +17 -0
  1346. data/lib/lcp_ruby/display/renderers/email_link.rb +15 -0
  1347. data/lib/lcp_ruby/display/renderers/file_size.rb +11 -0
  1348. data/lib/lcp_ruby/display/renderers/heading.rb +11 -0
  1349. data/lib/lcp_ruby/display/renderers/image.rb +17 -0
  1350. data/lib/lcp_ruby/display/renderers/internal_link.rb +23 -0
  1351. data/lib/lcp_ruby/display/renderers/link.rb +15 -0
  1352. data/lib/lcp_ruby/display/renderers/link_list.rb +90 -0
  1353. data/lib/lcp_ruby/display/renderers/markdown.rb +33 -0
  1354. data/lib/lcp_ruby/display/renderers/number.rb +14 -0
  1355. data/lib/lcp_ruby/display/renderers/percentage.rb +12 -0
  1356. data/lib/lcp_ruby/display/renderers/phone_link.rb +15 -0
  1357. data/lib/lcp_ruby/display/renderers/progress_bar.rb +15 -0
  1358. data/lib/lcp_ruby/display/renderers/rating.rb +15 -0
  1359. data/lib/lcp_ruby/display/renderers/record_link.rb +101 -0
  1360. data/lib/lcp_ruby/display/renderers/relative_date.rb +15 -0
  1361. data/lib/lcp_ruby/display/renderers/rich_text.rb +15 -0
  1362. data/lib/lcp_ruby/display/renderers/text.rb +12 -0
  1363. data/lib/lcp_ruby/display/renderers/truncate.rb +17 -0
  1364. data/lib/lcp_ruby/display/renderers/url_link.rb +22 -0
  1365. data/lib/lcp_ruby/display/renderers/workflow_badge.rb +37 -0
  1366. data/lib/lcp_ruby/display/renderers/workflow_timeline.rb +173 -0
  1367. data/lib/lcp_ruby/display/renderers.rb +3 -0
  1368. data/lib/lcp_ruby/display/text_badge.rb +15 -0
  1369. data/lib/lcp_ruby/dsl/condition_builder.rb +190 -0
  1370. data/lib/lcp_ruby/dsl/dsl_loader.rb +365 -0
  1371. data/lib/lcp_ruby/dsl/field_builder.rb +35 -0
  1372. data/lib/lcp_ruby/dsl/job_builder.rb +92 -0
  1373. data/lib/lcp_ruby/dsl/model_builder.rb +544 -0
  1374. data/lib/lcp_ruby/dsl/presenter_builder.rb +1272 -0
  1375. data/lib/lcp_ruby/dsl/source_location_capture.rb +52 -0
  1376. data/lib/lcp_ruby/dsl/type_builder.rb +88 -0
  1377. data/lib/lcp_ruby/dsl/view_group_builder.rb +92 -0
  1378. data/lib/lcp_ruby/dsl/workflow_builder.rb +319 -0
  1379. data/lib/lcp_ruby/dynamic.rb +7 -0
  1380. data/lib/lcp_ruby/dynamic_references/resolver.rb +154 -0
  1381. data/lib/lcp_ruby/dynamic_references/validator.rb +92 -0
  1382. data/lib/lcp_ruby/embed_providers/base.rb +18 -0
  1383. data/lib/lcp_ruby/embed_providers/grafana.rb +38 -0
  1384. data/lib/lcp_ruby/embed_providers/metabase.rb +37 -0
  1385. data/lib/lcp_ruby/engine.rb +680 -0
  1386. data/lib/lcp_ruby/events/async_handler_job.rb +21 -0
  1387. data/lib/lcp_ruby/events/dispatcher.rb +52 -0
  1388. data/lib/lcp_ruby/events/handler_base.rb +51 -0
  1389. data/lib/lcp_ruby/events/handler_registry.rb +49 -0
  1390. data/lib/lcp_ruby/export/data_generator.rb +158 -0
  1391. data/lib/lcp_ruby/export/export_handler.rb +315 -0
  1392. data/lib/lcp_ruby/export/field_tree_builder.rb +219 -0
  1393. data/lib/lcp_ruby/export/setup.rb +94 -0
  1394. data/lib/lcp_ruby/export/value_formatter.rb +223 -0
  1395. data/lib/lcp_ruby/export.rb +9 -0
  1396. data/lib/lcp_ruby/gem_paths.rb +51 -0
  1397. data/lib/lcp_ruby/generators/entity_menu_writer.rb +258 -0
  1398. data/lib/lcp_ruby/generators/feature_registry.rb +208 -0
  1399. data/lib/lcp_ruby/generators/prerequisites.rb +90 -0
  1400. data/lib/lcp_ruby/grouped_query/builder.rb +206 -0
  1401. data/lib/lcp_ruby/grouped_query/result_wrapper.rb +72 -0
  1402. data/lib/lcp_ruby/grouped_query/row.rb +31 -0
  1403. data/lib/lcp_ruby/groups/change_handler.rb +18 -0
  1404. data/lib/lcp_ruby/groups/contract.rb +42 -0
  1405. data/lib/lcp_ruby/groups/contract_validator.rb +110 -0
  1406. data/lib/lcp_ruby/groups/host_loader.rb +54 -0
  1407. data/lib/lcp_ruby/groups/model_loader.rb +186 -0
  1408. data/lib/lcp_ruby/groups/registry.rb +113 -0
  1409. data/lib/lcp_ruby/groups/setup.rb +129 -0
  1410. data/lib/lcp_ruby/groups/yaml_loader.rb +97 -0
  1411. data/lib/lcp_ruby/hash_utils.rb +42 -0
  1412. data/lib/lcp_ruby/i18n_check/configuration.rb +104 -0
  1413. data/lib/lcp_ruby/i18n_check/heuristics.rb +26 -0
  1414. data/lib/lcp_ruby/i18n_check/key_deriver.rb +136 -0
  1415. data/lib/lcp_ruby/i18n_check/offense.rb +29 -0
  1416. data/lib/lcp_ruby/i18n_check/registry_walker.rb +621 -0
  1417. data/lib/lcp_ruby/i18n_check/reporter.rb +96 -0
  1418. data/lib/lcp_ruby/i18n_check/runner.rb +46 -0
  1419. data/lib/lcp_ruby/i18n_check.rb +15 -0
  1420. data/lib/lcp_ruby/i18n_lint.rb +145 -0
  1421. data/lib/lcp_ruby/import/auto_mapper.rb +98 -0
  1422. data/lib/lcp_ruby/import/field_tree_builder.rb +178 -0
  1423. data/lib/lcp_ruby/import/file_parser.rb +223 -0
  1424. data/lib/lcp_ruby/import/import_dialog_handler.rb +410 -0
  1425. data/lib/lcp_ruby/import/import_job_handler.rb +224 -0
  1426. data/lib/lcp_ruby/import/row_processor.rb +281 -0
  1427. data/lib/lcp_ruby/import/setup.rb +277 -0
  1428. data/lib/lcp_ruby/import/value_coercer.rb +143 -0
  1429. data/lib/lcp_ruby/import.rb +14 -0
  1430. data/lib/lcp_ruby/json_item_wrapper.rb +152 -0
  1431. data/lib/lcp_ruby/kanban/board.rb +28 -0
  1432. data/lib/lcp_ruby/kanban/column.rb +28 -0
  1433. data/lib/lcp_ruby/kanban/default_provider.rb +376 -0
  1434. data/lib/lcp_ruby/kanban/host_provider.rb +94 -0
  1435. data/lib/lcp_ruby/kanban/move_result.rb +52 -0
  1436. data/lib/lcp_ruby/kanban/provider_test_harness.rb +54 -0
  1437. data/lib/lcp_ruby/kanban/swimlane.rb +21 -0
  1438. data/lib/lcp_ruby/menu.rb +46 -0
  1439. data/lib/lcp_ruby/metadata/aggregate_definition.rb +6 -0
  1440. data/lib/lcp_ruby/metadata/association_definition.rb +196 -0
  1441. data/lib/lcp_ruby/metadata/auth_validator.rb +222 -0
  1442. data/lib/lcp_ruby/metadata/configuration_validator.rb +7958 -0
  1443. data/lib/lcp_ruby/metadata/contract_result.rb +9 -0
  1444. data/lib/lcp_ruby/metadata/display_template_definition.rb +77 -0
  1445. data/lib/lcp_ruby/metadata/enum_label_resolver.rb +27 -0
  1446. data/lib/lcp_ruby/metadata/erd_generator.rb +274 -0
  1447. data/lib/lcp_ruby/metadata/event_definition.rb +55 -0
  1448. data/lib/lcp_ruby/metadata/field_definition.rb +267 -0
  1449. data/lib/lcp_ruby/metadata/group_definition.rb +31 -0
  1450. data/lib/lcp_ruby/metadata/i18n_label.rb +29 -0
  1451. data/lib/lcp_ruby/metadata/loader.rb +916 -0
  1452. data/lib/lcp_ruby/metadata/menu_definition.rb +116 -0
  1453. data/lib/lcp_ruby/metadata/menu_item.rb +792 -0
  1454. data/lib/lcp_ruby/metadata/menu_item_resolver.rb +105 -0
  1455. data/lib/lcp_ruby/metadata/model_definition.rb +612 -0
  1456. data/lib/lcp_ruby/metadata/model_hash_merger.rb +88 -0
  1457. data/lib/lcp_ruby/metadata/model_inheritance_resolver.rb +165 -0
  1458. data/lib/lcp_ruby/metadata/page_definition.rb +245 -0
  1459. data/lib/lcp_ruby/metadata/path_template.rb +231 -0
  1460. data/lib/lcp_ruby/metadata/permission_definition.rb +237 -0
  1461. data/lib/lcp_ruby/metadata/permission_merger.rb +81 -0
  1462. data/lib/lcp_ruby/metadata/presenter_definition.rb +689 -0
  1463. data/lib/lcp_ruby/metadata/reserved_names.rb +79 -0
  1464. data/lib/lcp_ruby/metadata/responsive_policy.rb +95 -0
  1465. data/lib/lcp_ruby/metadata/schema_validator.rb +172 -0
  1466. data/lib/lcp_ruby/metadata/validation_definition.rb +69 -0
  1467. data/lib/lcp_ruby/metadata/view_group_definition.rb +208 -0
  1468. data/lib/lcp_ruby/metadata/virtual_column_definition.rb +154 -0
  1469. data/lib/lcp_ruby/metadata/zone_definition.rb +423 -0
  1470. data/lib/lcp_ruby/metrics/collector.rb +123 -0
  1471. data/lib/lcp_ruby/metrics/collector_registry.rb +57 -0
  1472. data/lib/lcp_ruby/metrics/error_recorder.rb +70 -0
  1473. data/lib/lcp_ruby/metrics/fingerprint.rb +33 -0
  1474. data/lib/lcp_ruby/metrics/json_query.rb +47 -0
  1475. data/lib/lcp_ruby/metrics/metric_definitions.rb +105 -0
  1476. data/lib/lcp_ruby/metrics/prometheus_check.rb +9 -0
  1477. data/lib/lcp_ruby/metrics/rate_limiter.rb +99 -0
  1478. data/lib/lcp_ruby/metrics/setup.rb +71 -0
  1479. data/lib/lcp_ruby/metrics/subscriber.rb +126 -0
  1480. data/lib/lcp_ruby/model_factory/aggregate_applicator.rb +14 -0
  1481. data/lib/lcp_ruby/model_factory/api_association_applicator.rb +119 -0
  1482. data/lib/lcp_ruby/model_factory/api_builder.rb +85 -0
  1483. data/lib/lcp_ruby/model_factory/array_type.rb +78 -0
  1484. data/lib/lcp_ruby/model_factory/array_type_applicator.rb +33 -0
  1485. data/lib/lcp_ruby/model_factory/association_applicator.rb +201 -0
  1486. data/lib/lcp_ruby/model_factory/attachment_applicator.rb +160 -0
  1487. data/lib/lcp_ruby/model_factory/auditing_applicator.rb +72 -0
  1488. data/lib/lcp_ruby/model_factory/builder.rb +235 -0
  1489. data/lib/lcp_ruby/model_factory/callback_applicator.rb +63 -0
  1490. data/lib/lcp_ruby/model_factory/computed_applicator.rb +55 -0
  1491. data/lib/lcp_ruby/model_factory/default_applicator.rb +85 -0
  1492. data/lib/lcp_ruby/model_factory/enum_applicator.rb +24 -0
  1493. data/lib/lcp_ruby/model_factory/inherited_parent_validator_applicator.rb +39 -0
  1494. data/lib/lcp_ruby/model_factory/label_method_builder.rb +82 -0
  1495. data/lib/lcp_ruby/model_factory/managed_tracking.rb +71 -0
  1496. data/lib/lcp_ruby/model_factory/positioning_applicator.rb +23 -0
  1497. data/lib/lcp_ruby/model_factory/ransack_applicator.rb +66 -0
  1498. data/lib/lcp_ruby/model_factory/registry.rb +33 -0
  1499. data/lib/lcp_ruby/model_factory/schema_manager.rb +655 -0
  1500. data/lib/lcp_ruby/model_factory/scope_applicator.rb +87 -0
  1501. data/lib/lcp_ruby/model_factory/sequence_applicator.rb +173 -0
  1502. data/lib/lcp_ruby/model_factory/service_accessor_applicator.rb +40 -0
  1503. data/lib/lcp_ruby/model_factory/soft_delete_applicator.rb +141 -0
  1504. data/lib/lcp_ruby/model_factory/transform_applicator.rb +51 -0
  1505. data/lib/lcp_ruby/model_factory/tree_applicator.rb +239 -0
  1506. data/lib/lcp_ruby/model_factory/userstamps_applicator.rb +73 -0
  1507. data/lib/lcp_ruby/model_factory/validation_applicator.rb +319 -0
  1508. data/lib/lcp_ruby/model_factory/virtual_column_applicator.rb +79 -0
  1509. data/lib/lcp_ruby/model_factory/workflow_applicator.rb +141 -0
  1510. data/lib/lcp_ruby/pages/change_handler.rb +15 -0
  1511. data/lib/lcp_ruby/pages/contract_validator.rb +74 -0
  1512. data/lib/lcp_ruby/pages/definition_validator.rb +42 -0
  1513. data/lib/lcp_ruby/pages/filter_form.rb +200 -0
  1514. data/lib/lcp_ruby/pages/filter_form_validator.rb +636 -0
  1515. data/lib/lcp_ruby/pages/registry.rb +133 -0
  1516. data/lib/lcp_ruby/pages/resolver.rb +32 -0
  1517. data/lib/lcp_ruby/pages/scope_context_resolver.rb +37 -0
  1518. data/lib/lcp_ruby/pages/scope_filter_set.rb +57 -0
  1519. data/lib/lcp_ruby/pages/setup.rb +46 -0
  1520. data/lib/lcp_ruby/path_utils.rb +12 -0
  1521. data/lib/lcp_ruby/permissions/change_handler.rb +22 -0
  1522. data/lib/lcp_ruby/permissions/contract_validator.rb +74 -0
  1523. data/lib/lcp_ruby/permissions/definition_validator.rb +119 -0
  1524. data/lib/lcp_ruby/permissions/registry.rb +135 -0
  1525. data/lib/lcp_ruby/permissions/setup.rb +51 -0
  1526. data/lib/lcp_ruby/permissions/source_resolver.rb +56 -0
  1527. data/lib/lcp_ruby/presenter/action_set.rb +236 -0
  1528. data/lib/lcp_ruby/presenter/breadcrumb_builder.rb +183 -0
  1529. data/lib/lcp_ruby/presenter/breadcrumb_path_helper.rb +17 -0
  1530. data/lib/lcp_ruby/presenter/column_set.rb +268 -0
  1531. data/lib/lcp_ruby/presenter/enrichment.rb +136 -0
  1532. data/lib/lcp_ruby/presenter/field_value_resolver.rb +237 -0
  1533. data/lib/lcp_ruby/presenter/includes_resolver/association_dependency.rb +59 -0
  1534. data/lib/lcp_ruby/presenter/includes_resolver/dependency_collector.rb +394 -0
  1535. data/lib/lcp_ruby/presenter/includes_resolver/loading_strategy.rb +70 -0
  1536. data/lib/lcp_ruby/presenter/includes_resolver/strategy_resolver.rb +123 -0
  1537. data/lib/lcp_ruby/presenter/includes_resolver.rb +42 -0
  1538. data/lib/lcp_ruby/presenter/layout_builder.rb +467 -0
  1539. data/lib/lcp_ruby/presenter/link_resolver.rb +65 -0
  1540. data/lib/lcp_ruby/presenter/metadata_lookup.rb +28 -0
  1541. data/lib/lcp_ruby/presenter/resolver.rb +25 -0
  1542. data/lib/lcp_ruby/record_aliases/metadata_checker.rb +213 -0
  1543. data/lib/lcp_ruby/record_aliases/setup.rb +212 -0
  1544. data/lib/lcp_ruby/reserved_route_segments.rb +37 -0
  1545. data/lib/lcp_ruby/roles/change_handler.rb +11 -0
  1546. data/lib/lcp_ruby/roles/contract_validator.rb +67 -0
  1547. data/lib/lcp_ruby/roles/registry.rb +89 -0
  1548. data/lib/lcp_ruby/roles/setup.rb +50 -0
  1549. data/lib/lcp_ruby/routing/presenter_routes.rb +104 -0
  1550. data/lib/lcp_ruby/saved_filters/change_handler.rb +13 -0
  1551. data/lib/lcp_ruby/saved_filters/contract_validator.rb +85 -0
  1552. data/lib/lcp_ruby/saved_filters/registry.rb +36 -0
  1553. data/lib/lcp_ruby/saved_filters/resolver.rb +108 -0
  1554. data/lib/lcp_ruby/saved_filters/setup.rb +42 -0
  1555. data/lib/lcp_ruby/saved_filters/stale_field_validator.rb +84 -0
  1556. data/lib/lcp_ruby/schemas/auth.json +208 -0
  1557. data/lib/lcp_ruby/schemas/menu.json +338 -0
  1558. data/lib/lcp_ruby/schemas/model.json +1161 -0
  1559. data/lib/lcp_ruby/schemas/page.json +877 -0
  1560. data/lib/lcp_ruby/schemas/permission.json +454 -0
  1561. data/lib/lcp_ruby/schemas/presenter.json +2274 -0
  1562. data/lib/lcp_ruby/schemas/theme.json +62 -0
  1563. data/lib/lcp_ruby/schemas/type.json +146 -0
  1564. data/lib/lcp_ruby/schemas/view_group.json +163 -0
  1565. data/lib/lcp_ruby/search/custom_field_filter.rb +171 -0
  1566. data/lib/lcp_ruby/search/custom_filter_interceptor.rb +40 -0
  1567. data/lib/lcp_ruby/search/filter_metadata_builder.rb +409 -0
  1568. data/lib/lcp_ruby/search/filter_param_builder.rb +177 -0
  1569. data/lib/lcp_ruby/search/operator_registry.rb +79 -0
  1570. data/lib/lcp_ruby/search/param_sanitizer.rb +25 -0
  1571. data/lib/lcp_ruby/search/parameter_definition.rb +187 -0
  1572. data/lib/lcp_ruby/search/parameterized_scope_applicator.rb +129 -0
  1573. data/lib/lcp_ruby/search/query_builder.rb +143 -0
  1574. data/lib/lcp_ruby/search/query_language_parser.rb +549 -0
  1575. data/lib/lcp_ruby/search/query_language_serializer.rb +193 -0
  1576. data/lib/lcp_ruby/search/quick_search.rb +162 -0
  1577. data/lib/lcp_ruby/search/relative_date_expander.rb +57 -0
  1578. data/lib/lcp_ruby/search_result.rb +70 -0
  1579. data/lib/lcp_ruby/sequences/sequence_manager.rb +51 -0
  1580. data/lib/lcp_ruby/services/accessors/json_field.rb +23 -0
  1581. data/lib/lcp_ruby/services/built_in_accessors.rb +17 -0
  1582. data/lib/lcp_ruby/services/built_in_defaults.rb +22 -0
  1583. data/lib/lcp_ruby/services/built_in_transforms.rb +20 -0
  1584. data/lib/lcp_ruby/services/checker.rb +133 -0
  1585. data/lib/lcp_ruby/services/registry.rb +83 -0
  1586. data/lib/lcp_ruby/skills_installer.rb +73 -0
  1587. data/lib/lcp_ruby/sort/enum_sort_order.rb +38 -0
  1588. data/lib/lcp_ruby/tasks/destroy_order_resolver.rb +57 -0
  1589. data/lib/lcp_ruby/tasks/doctor.rb +294 -0
  1590. data/lib/lcp_ruby/tasks/permission_resolve_formatter.rb +245 -0
  1591. data/lib/lcp_ruby/types/built_in_types.rb +157 -0
  1592. data/lib/lcp_ruby/types/transforms/base_transform.rb +11 -0
  1593. data/lib/lcp_ruby/types/transforms/downcase.rb +11 -0
  1594. data/lib/lcp_ruby/types/transforms/normalize_phone.rb +19 -0
  1595. data/lib/lcp_ruby/types/transforms/normalize_url.rb +16 -0
  1596. data/lib/lcp_ruby/types/transforms/strip.rb +11 -0
  1597. data/lib/lcp_ruby/types/type_definition.rb +112 -0
  1598. data/lib/lcp_ruby/types/type_registry.rb +75 -0
  1599. data/lib/lcp_ruby/url_safety.rb +97 -0
  1600. data/lib/lcp_ruby/user_snapshot.rb +15 -0
  1601. data/lib/lcp_ruby/version.rb +3 -0
  1602. data/lib/lcp_ruby/view_slots/registry.rb +71 -0
  1603. data/lib/lcp_ruby/view_slots/slot_component.rb +22 -0
  1604. data/lib/lcp_ruby/view_slots/slot_context.rb +20 -0
  1605. data/lib/lcp_ruby/virtual_columns/builder.rb +234 -0
  1606. data/lib/lcp_ruby/virtual_columns/collector.rb +186 -0
  1607. data/lib/lcp_ruby/virtual_columns.rb +4 -0
  1608. data/lib/lcp_ruby/virtual_fields/synthetic_marker.rb +17 -0
  1609. data/lib/lcp_ruby/virtual_fields/types/array_of.rb +49 -0
  1610. data/lib/lcp_ruby/virtual_fields/virtual_field.rb +107 -0
  1611. data/lib/lcp_ruby/virtual_fields/virtual_form.rb +144 -0
  1612. data/lib/lcp_ruby/widgets/chart_palette.rb +25 -0
  1613. data/lib/lcp_ruby/widgets/chartkick_check.rb +9 -0
  1614. data/lib/lcp_ruby/widgets/data_resolver.rb +676 -0
  1615. data/lib/lcp_ruby/widgets/date_grouper.rb +54 -0
  1616. data/lib/lcp_ruby/widgets/presenter_zone_resolver.rb +170 -0
  1617. data/lib/lcp_ruby/widgets/record_source_resolver.rb +56 -0
  1618. data/lib/lcp_ruby/widgets/scope_applicator.rb +187 -0
  1619. data/lib/lcp_ruby/workflow/approval/activation_handler.rb +39 -0
  1620. data/lib/lcp_ruby/workflow/approval/approval_definition.rb +117 -0
  1621. data/lib/lcp_ruby/workflow/approval/approver_resolver.rb +98 -0
  1622. data/lib/lcp_ruby/workflow/approval/cleanup_handler.rb +37 -0
  1623. data/lib/lcp_ruby/workflow/approval/contract_validator.rb +96 -0
  1624. data/lib/lcp_ruby/workflow/approval/data_builder.rb +53 -0
  1625. data/lib/lcp_ruby/workflow/approval/discard_handler.rb +51 -0
  1626. data/lib/lcp_ruby/workflow/approval/engine.rb +314 -0
  1627. data/lib/lcp_ruby/workflow/approval/registry.rb +40 -0
  1628. data/lib/lcp_ruby/workflow/approval/resolution_handler.rb +103 -0
  1629. data/lib/lcp_ruby/workflow/approval/setup.rb +138 -0
  1630. data/lib/lcp_ruby/workflow/approval/step_definition.rb +52 -0
  1631. data/lib/lcp_ruby/workflow/approval/step_evaluator.rb +163 -0
  1632. data/lib/lcp_ruby/workflow/approval/system_evaluator.rb +29 -0
  1633. data/lib/lcp_ruby/workflow/approval/task_manager.rb +202 -0
  1634. data/lib/lcp_ruby/workflow/audit_contract_validator.rb +64 -0
  1635. data/lib/lcp_ruby/workflow/audit_registry.rb +24 -0
  1636. data/lib/lcp_ruby/workflow/audit_writer.rb +51 -0
  1637. data/lib/lcp_ruby/workflow/change_handler.rb +14 -0
  1638. data/lib/lcp_ruby/workflow/contract.rb +21 -0
  1639. data/lib/lcp_ruby/workflow/contract_validator.rb +44 -0
  1640. data/lib/lcp_ruby/workflow/errors.rb +12 -0
  1641. data/lib/lcp_ruby/workflow/host_source.rb +19 -0
  1642. data/lib/lcp_ruby/workflow/mermaid_builder.rb +217 -0
  1643. data/lib/lcp_ruby/workflow/model_source.rb +79 -0
  1644. data/lib/lcp_ruby/workflow/registry.rb +113 -0
  1645. data/lib/lcp_ruby/workflow/resolver.rb +32 -0
  1646. data/lib/lcp_ruby/workflow/setup.rb +135 -0
  1647. data/lib/lcp_ruby/workflow/state_definition.rb +59 -0
  1648. data/lib/lcp_ruby/workflow/state_machine.rb +78 -0
  1649. data/lib/lcp_ruby/workflow/static_source.rb +20 -0
  1650. data/lib/lcp_ruby/workflow/transition_action_builder.rb +46 -0
  1651. data/lib/lcp_ruby/workflow/transition_definition.rb +70 -0
  1652. data/lib/lcp_ruby/workflow/transition_executor.rb +140 -0
  1653. data/lib/lcp_ruby/workflow/transition_label_resolver.rb +21 -0
  1654. data/lib/lcp_ruby/workflow/transition_result.rb +20 -0
  1655. data/lib/lcp_ruby/workflow/value_resolver.rb +58 -0
  1656. data/lib/lcp_ruby/workflow/workflow_definition.rb +195 -0
  1657. data/lib/lcp_ruby.rb +764 -0
  1658. data/lib/rubocop/cop/lcp_ruby/no_hardcoded_i18n_string.rb +249 -0
  1659. data/lib/tasks/lcp_ruby.rake +432 -0
  1660. data/lib/tasks/lcp_ruby_assets.rake +37 -0
  1661. data/lib/tasks/lcp_ruby_auth.rake +49 -0
  1662. data/lib/tasks/lcp_ruby_db.rake +76 -0
  1663. data/lib/tasks/lcp_ruby_doctor.rake +20 -0
  1664. data/lib/tasks/lcp_ruby_feature_catalog.rake +61 -0
  1665. data/lib/tasks/lcp_ruby_gapfree_sequences.rake +39 -0
  1666. data/lib/tasks/lcp_ruby_i18n_check.rake +23 -0
  1667. data/lib/tasks/lcp_ruby_i18n_lint.rake +20 -0
  1668. data/lib/tasks/lcp_ruby_invariant_check.rake +72 -0
  1669. data/vendor/assets/javascripts/lcp_ruby/activestorage.min.js +866 -0
  1670. data/vendor/assets/javascripts/lcp_ruby/highlight.min.js +1244 -0
  1671. data/vendor/assets/javascripts/lcp_ruby/lucide.min.js +12 -0
  1672. data/vendor/assets/javascripts/lcp_ruby/stimulus.umd.js +2588 -0
  1673. data/vendor/assets/javascripts/lcp_ruby/tom-select.complete.min.js +444 -0
  1674. data/vendor/assets/stylesheets/lcp_ruby/highlight-github.min.css +12 -0
  1675. data/vendor/assets/stylesheets/lcp_ruby/tom-select.css +412 -0
  1676. metadata +1950 -0
@@ -0,0 +1,2972 @@
1
+ # Models Reference
2
+
3
+ File: `config/lcp_ruby/models/<name>.yml`
4
+
5
+ Model YAML defines the data layer: database columns, validations, associations, scopes, and events. The engine reads these files at boot and dynamically creates ActiveRecord models under the `LcpRuby::Dynamic::` namespace.
6
+
7
+ ## Top-Level Attributes
8
+
9
+ ```yaml
10
+ model:
11
+ name: <model_name>
12
+ label: "Display Name"
13
+ label_plural: "Display Names"
14
+ table_name: <custom_table_name>
15
+ slug_field: <field_name>
16
+ fields: []
17
+ validations: []
18
+ associations: []
19
+ scopes: []
20
+ events: []
21
+ display_templates: {}
22
+ virtual_columns: {} # computed query-time columns (expressions, JOINs, aggregates)
23
+ aggregates: {} # legacy alias for virtual_columns, still supported
24
+ positioning: true | { field: position, scope: parent_id }
25
+ options: {}
26
+ ```
27
+
28
+ ### `name`
29
+
30
+ | | |
31
+ |---|---|
32
+ | **Required** | yes |
33
+ | **Type** | string (snake_case) |
34
+
35
+ Internal identifier for the model. Used to generate the AR class name (`LcpRuby::Dynamic::TodoItem` for `todo_item`), default table name, and cross-references from presenters and permissions.
36
+
37
+ ### `label`
38
+
39
+ | | |
40
+ |---|---|
41
+ | **Required** | no |
42
+ | **Default** | `name.humanize` |
43
+ | **Type** | string |
44
+
45
+ Human-readable singular name displayed in the UI (page titles, flash messages).
46
+
47
+ ### `label_plural`
48
+
49
+ | | |
50
+ |---|---|
51
+ | **Required** | no |
52
+ | **Default** | `label.pluralize` |
53
+ | **Type** | string |
54
+
55
+ Plural form of the label. Used in index page headings and navigation.
56
+
57
+ ### `abstract`
58
+
59
+ | | |
60
+ |---|---|
61
+ | **Required** | no |
62
+ | **Default** | `false` |
63
+ | **Type** | boolean |
64
+
65
+ When `true`, this model serves as a reusable template. Abstract models produce no AR class or DB table — their fields, associations, scopes, events, validations, options, virtual_columns, display_templates, and indexes are merged into child models that reference them via `inherits`. Abstract models must not define `table_name` or `options.sti`.
66
+
67
+ **Convention:** Abstract model names start with `_` (underscore) to signal "not instantiable." This is a convention, not enforced — the `abstract: true` key is the authoritative flag.
68
+
69
+ ```yaml
70
+ model:
71
+ name: _auditable
72
+ abstract: true
73
+ fields:
74
+ - name: created_by
75
+ type: string
76
+ - name: updated_by
77
+ type: string
78
+ options:
79
+ userstamps: true
80
+ ```
81
+
82
+ ### `inherits`
83
+
84
+ | | |
85
+ |---|---|
86
+ | **Required** | no |
87
+ | **Type** | string |
88
+
89
+ Name of a parent model whose fields, associations, scopes, events, validations, options, virtual_columns, display_templates, and indexes are merged into this model. The parent model must be either `abstract: true` or have `options.sti: true`.
90
+
91
+ **Two inheritance modes:**
92
+
93
+ 1. **Abstract inheritance** — parent has `abstract: true`. Merged at metadata level; parent produces no AR class or DB table.
94
+ 2. **STI inheritance** — parent has `options.sti: true`. Child AR class inherits from parent AR class and shares the parent's table via [Single Table Inheritance](#single-table-inheritance-sti).
95
+
96
+ **Merge rules:**
97
+
98
+ | Section | Behavior |
99
+ |---------|----------|
100
+ | `fields` | Parent fields first, then child fields appended. Child field with same name **overrides** parent field entirely. |
101
+ | `associations` | Parent first, child appended. Same-name override. |
102
+ | `scopes` | Parent first, child appended. Same-name override. |
103
+ | `events` | Parent first, child appended. Same-name override. |
104
+ | `validations` | Additive — parent validations concatenated with child. No override. |
105
+ | `options` | Deep merge — child options override parent where keys collide. |
106
+ | `virtual_columns` | Merge by key — child overrides parent on collision. |
107
+ | `display_templates` | Merge by key — child overrides parent on collision. |
108
+ | `indexes` | Concatenated (all inherited + all child). |
109
+ | `positioning` | Child wins if present, otherwise inherited. |
110
+ | `data_source` | Child wins if present, otherwise inherited. |
111
+ | `name`, `table_name`, `label`, `label_plural` | Never inherited — always the child's own identity. |
112
+
113
+ **Chaining:** A model can inherit from a parent that itself inherits from another abstract model. An abstract → STI chain (abstract parent → STI parent → STI child) is supported. Circular inheritance is detected and raises `MetadataError` at load time.
114
+
115
+ **Cross-format:** YAML models can inherit from DSL-defined abstract models and vice versa.
116
+
117
+ ```yaml
118
+ model:
119
+ name: ticket
120
+ inherits: _auditable
121
+ fields:
122
+ - name: title
123
+ type: string
124
+ ```
125
+
126
+ ### `table_name`
127
+
128
+ | | |
129
+ |---|---|
130
+ | **Required** | no |
131
+ | **Default** | `name.pluralize` |
132
+ | **Type** | string |
133
+
134
+ Override the database table name. Use this when integrating with an existing database that has a non-standard table naming convention.
135
+
136
+ ```yaml
137
+ model:
138
+ name: todo_item
139
+ table_name: legacy_todo_items
140
+ ```
141
+
142
+ #### Virtual Models
143
+
144
+ Set `table_name: _virtual` to create a metadata-only model — no database table and no ActiveRecord class are created. Virtual models exist purely for field metadata (types, labels, validations, transforms, defaults) and are used as item definitions for JSON field nested editing.
145
+
146
+ ```yaml
147
+ model:
148
+ name: address
149
+ table_name: _virtual
150
+ fields:
151
+ - name: street
152
+ type: string
153
+ validations: [{ type: presence }]
154
+ - name: city
155
+ type: string
156
+ validations: [{ type: presence }]
157
+ - name: zip
158
+ type: string
159
+ - name: country
160
+ type: string
161
+ default: "CZ"
162
+ ```
163
+
164
+ Virtual models:
165
+ - Are loaded by `Metadata::Loader` and available via `LcpRuby.loader.model_definitions`
166
+ - Are **not** registered in `LcpRuby.registry` (no AR class to register)
167
+ - Are **not** built by `ModelFactory::Builder` — `SchemaManager.ensure_table!` skips them
168
+ - Should not have associations or scopes (these produce warnings in `ConfigurationValidator`)
169
+ - Are referenced from presenter `nested_fields` sections via `target_model:` — see [JSON Field with Target Model](presenters.md#json-field-source-model-backed)
170
+
171
+ When a `json_field:` + `target_model:` nested section is rendered, each hash item from the JSON array is wrapped in a `JsonItemWrapper` that uses the virtual model's field definitions for type coercion, getter/setter access, and per-item validation.
172
+
173
+ #### API-Backed Models
174
+
175
+ Add a `data_source` key to create a model backed by an external REST API or host-provided adapter instead of the database:
176
+
177
+ ```yaml
178
+ model:
179
+ name: external_building
180
+ data_source:
181
+ type: rest_json # rest_json | host
182
+ base_url: "https://api.example.com"
183
+ resource: "/buildings"
184
+ auth:
185
+ type: bearer
186
+ token_env: "API_TOKEN"
187
+ cache:
188
+ enabled: true
189
+ ttl: 300
190
+ fields:
191
+ - name: name
192
+ type: string
193
+ ```
194
+
195
+ API-backed models use `ActiveModel` instead of `ActiveRecord`. They support index and show views, cross-source associations (DB model → API model), permissions, and display renderers. Features requiring database access (soft_delete, auditing, userstamps, tree, positioning, custom_fields) are not available.
196
+
197
+ See [API-Backed Models Reference](api-backed-models.md) and [API-Backed Models Guide](../guides/api-backed-models.md) for full documentation.
198
+
199
+ ### `positioning`
200
+
201
+ | | |
202
+ |---|---|
203
+ | **Required** | no |
204
+ | **Type** | boolean or hash |
205
+
206
+ Enables automatic record positioning via the [`positioning`](https://github.com/brendon/positioning) gem. When present, records get automatic sequential position assignment on create, gap closing on destroy, and atomic reorder support.
207
+
208
+ **Minimal form** — uses `position` field with no scope:
209
+
210
+ ```yaml
211
+ positioning: true
212
+ ```
213
+
214
+ **Hash form** — custom field and/or scoped positioning:
215
+
216
+ ```yaml
217
+ positioning:
218
+ field: position # optional, default: "position"
219
+ scope: pipeline_id # optional, string or array of strings
220
+ ```
221
+
222
+ **Multi-column scope:**
223
+
224
+ ```yaml
225
+ positioning:
226
+ scope: [pipeline_id, category]
227
+ ```
228
+
229
+ When `positioning` is set:
230
+ - The `positioned` gem macro is applied to the dynamic model
231
+ - The position column gets a `NOT NULL` constraint
232
+ - A unique index on `[scope_columns..., position_column]` is added (except on SQLite)
233
+ - The `positioning.field` must reference a declared field of type `integer`
234
+ - Each `positioning.scope` entry must be a declared field or a belongs_to FK
235
+
236
+ See [Record Positioning](../design/record_positioning.md) for the full design.
237
+
238
+ ### `indexes`
239
+
240
+ | | |
241
+ |---|---|
242
+ | **Required** | no |
243
+ | **Default** | `[]` |
244
+ | **Type** | array of index objects |
245
+
246
+ Declares database indexes on the model's table. Each index specifies columns, optional uniqueness, and optional custom name.
247
+
248
+ ```yaml
249
+ indexes:
250
+ - columns: [seq_model, seq_field, scope_key]
251
+ unique: true
252
+ - columns: [department_id, created_at]
253
+ - columns: [email]
254
+ unique: true
255
+ name: idx_users_email_uniq
256
+ ```
257
+
258
+ | Key | Type | Default | Description |
259
+ |-----|------|---------|-------------|
260
+ | `columns` | array of strings | (required) | Column names to include in the index |
261
+ | `unique` | boolean | `false` | Create a unique constraint index |
262
+ | `name` | string | auto-generated | Custom index name |
263
+
264
+ Indexes are created at boot by `SchemaManager` (idempotent — existing indexes are skipped). All referenced columns must be defined fields on the model.
265
+
266
+ **DSL:**
267
+
268
+ ```ruby
269
+ define_model :gapfree_sequence do
270
+ field :seq_model, :string
271
+ field :seq_field, :string
272
+ field :scope_key, :string
273
+ index %i[seq_model seq_field scope_key], unique: true
274
+ end
275
+ ```
276
+
277
+ ### `slug_field`
278
+
279
+ | | |
280
+ |---|---|
281
+ | **Required** | no |
282
+ | **Default** | none |
283
+ | **Type** | string |
284
+
285
+ Name of a string field to use as URL slug. When set, the controller resolves non-numeric `params[:id]` via `find_by!(slug_field => param)` before falling back to numeric ID lookup. The dynamic model class also overrides `to_param` to return the slug value (with fallback to `id` when slug is nil/blank), so Rails URL helpers automatically generate slug-based URLs.
286
+
287
+ The field must be:
288
+ - A declared field on the model (not a virtual column)
289
+ - A string-compatible type (`string`, `email`, `url`, or a custom type resolving to a string column)
290
+ - Not an attachment field
291
+
292
+ The platform auto-adds an `exclusion` validation that prevents records from being saved with slug values that collide with reserved route segments (`new`, `edit`, `saved-filters`, etc.).
293
+
294
+ ```yaml
295
+ model:
296
+ name: article
297
+ slug_field: slug
298
+ fields:
299
+ - { name: title, type: string }
300
+ - { name: slug, type: string }
301
+ validations:
302
+ - { field: slug, type: uniqueness }
303
+ - { field: slug, type: format, options: { with: "\\A[a-z0-9-]+\\z" } }
304
+ indexes:
305
+ - { columns: [slug], unique: true }
306
+ ```
307
+
308
+ **Recommendations (validated as warnings at boot):**
309
+
310
+ - Add a `uniqueness` validation on the slug field — without it, `find_by!` returns non-deterministic results when duplicates exist.
311
+ - Use a format pattern that requires at least one non-numeric character — purely numeric slugs (e.g., `"123"`) are unreachable via URL because numeric params always resolve as record IDs.
312
+ - Use a lowercase-only format pattern — `find_by!` is case-sensitive on PostgreSQL, so `/articles/My-Slug` and `/articles/my-slug` would be different URLs.
313
+ - Avoid dots in slugs — Rails routing splits `params[:id]` on `.` into id and format (e.g., `/articles/v2.0` parses as `id: "v2", format: "0"`).
314
+ - Add a database index on the slug field — `find_by!` runs on every non-numeric request.
315
+
316
+ The recommended format pattern `\A[a-z0-9-]+\z` avoids all of the above pitfalls.
317
+
318
+ **STI note:** `slug_field` is not inherited by STI child models. Each child that needs slug resolution must declare its own `slug_field`. The `to_param` override is inherited via Ruby class hierarchy (so outbound links work), but the inbound resolve chain checks the child's own `ModelDefinition`.
319
+
320
+ **DSL:**
321
+
322
+ ```ruby
323
+ define_model :article do
324
+ slug_field :slug
325
+
326
+ field :slug, :string
327
+ field :title, :string
328
+ end
329
+ ```
330
+
331
+ See [Record Aliases Guide](../guides/record-aliases.md) for full examples.
332
+
333
+ ## Fields
334
+
335
+ Each field defines a database column and its metadata.
336
+
337
+ ```yaml
338
+ fields:
339
+ - name: title
340
+ type: string
341
+ label: "Title"
342
+ default: "Untitled"
343
+ column_options:
344
+ limit: 255
345
+ "null": false
346
+ enum_values: []
347
+ validations: []
348
+ ```
349
+
350
+ ### Field Attributes
351
+
352
+ #### `name`
353
+
354
+ | | |
355
+ |---|---|
356
+ | **Required** | yes |
357
+ | **Type** | string |
358
+
359
+ Column name. Must be unique within the model. Used as the ActiveRecord attribute name.
360
+
361
+ **Reserved names:** Some field names conflict with ActiveRecord/ActiveModel internals and must be avoided. Using a reserved name causes `ActiveModel::DangerousAttributeError` at runtime or silently overrides framework methods.
362
+
363
+ Names that **must not** be used as field names:
364
+
365
+ | Category | Names |
366
+ |----------|-------|
367
+ | Identity / lifecycle | `id`, `type`, `class`, `hash`, `object_id` |
368
+ | Persistence | `save`, `save!`, `destroy`, `destroy!`, `delete`, `update`, `update!`, `create` |
369
+ | State | `new_record?`, `persisted?`, `frozen?`, `valid?`, `invalid?`, `errors`, `changed?`, `changes` |
370
+ | Introspection | `model_name`, `respond_to?`, `send`, `inspect`, `to_s`, `to_param`, `to_model` |
371
+ | Attributes | `reload`, `assign_attributes`, `becomes`, `attributes`, `read_attribute`, `write_attribute`, `attribute_names`, `serializable_hash` |
372
+ | Object | `dup`, `clone`, `freeze` |
373
+
374
+ Additionally, these names are reserved by the platform: `custom_data` (used by custom fields), `discarded_at` (used by soft delete), `position` (used by positioning).
375
+
376
+ > **Tip:** If you need a field conceptually named after a reserved word, add a prefix or suffix — e.g., use `ai_model` instead of `model_name`, or `record_type` instead of `type` (unless you intend STI).
377
+
378
+ #### `type`
379
+
380
+ | | |
381
+ |---|---|
382
+ | **Required** | yes |
383
+ | **Type** | string (base type or registered business type) |
384
+
385
+ Determines the database column type and default form input behavior. Accepts one of the 14 base types below, or any registered [business type](types.md) name (e.g., `email`, `phone`, `url`, `color`).
386
+
387
+ **Base types:**
388
+
389
+ | Type | DB Column | Description |
390
+ |------|-----------|-------------|
391
+ | `string` | `:string` | Short text (varchar). Default form input: text field. |
392
+ | `text` | `:text` | Long text. Default form input: textarea. |
393
+ | `integer` | `:integer` | Whole number. |
394
+ | `float` | `:float` | Floating-point number. |
395
+ | `decimal` | `:decimal` | Precise decimal. Use `column_options: { precision:, scale: }`. |
396
+ | `boolean` | `:boolean` | True/false. Default form input: checkbox. |
397
+ | `date` | `:date` | Date without time. |
398
+ | `datetime` | `:datetime` | Date and time. |
399
+ | `enum` | `:string` | Stored as string. Requires `enum_values`. Default form input: select. |
400
+ | `file` | `:string` | File path/reference stored as string. |
401
+ | `rich_text` | `:text` | Rich text content. Default form input: WYSIWYG editor (Trix). Requires Action Text — `lcp new` installs it by default; if you used `--skip-action-text` or added LCP to an existing app without Action Text, the field silently renders as a plain textarea. The boot logger prints a warning naming each affected field with recovery steps (`bin/rails action_text:install` → `db:migrate` → restart). |
402
+ | `json` | `:jsonb` / `:json` | JSON data. Uses jsonb on PostgreSQL, json on other adapters. |
403
+ | `uuid` | `:string` | UUID stored as string. |
404
+ | `array` | PG: native array / SQLite: `:json` | Typed array of scalars. Requires `item_type`. Default form input: tag chips. |
405
+ | `attachment` | none (uses Active Storage) | File attachment (single or multiple). Requires Active Storage. |
406
+
407
+ **Built-in business types:**
408
+
409
+ | Type | Base | Transforms | Input | Description |
410
+ |------|------|------------|-------|-------------|
411
+ | `email` | string | strip, downcase | `<input type="email">` | Email with format validation |
412
+ | `phone` | string | strip, normalize_phone | `<input type="tel">` | Phone with format validation |
413
+ | `url` | string | strip, normalize_url | `<input type="url">` | URL with format validation, auto-prepends `https://` |
414
+ | `color` | string | strip, downcase | `<input type="color">` | Hex color (`#rrggbb`) with format validation |
415
+ | `bigint` | integer | — | `<input type="number">` | 8-byte integer — use for FK columns that reference Rails' default `bigint` PK to avoid silent `INT`/`BIGINT` join mismatches. |
416
+
417
+ Business types bundle transforms (normalization), validations, HTML input hints, and column options into a reusable definition. See [Types Reference](types.md) for defining custom types.
418
+
419
+ ##### Attachment Fields
420
+
421
+ The `attachment` type uses Active Storage instead of a database column. It supports both single-file (`has_one_attached`) and multi-file (`has_many_attached`) attachments.
422
+
423
+ **Options:**
424
+
425
+ | Option | Type | Default | Description |
426
+ |--------|------|---------|-------------|
427
+ | `multiple` | boolean | `false` | Use `has_many_attached` instead of `has_one_attached` |
428
+ | `accept` | string | none | HTML `accept` attribute hint for file input (e.g., `"image/*"`). Not validated server-side — use `content_types` for validation. |
429
+ | `max_size` | string | global default | Maximum file size per file (e.g., `"10MB"`, `"512KB"`) |
430
+ | `min_size` | string | none | Minimum file size per file |
431
+ | `content_types` | array | global default | Allowed MIME types. Supports wildcards (e.g., `"image/*"`) |
432
+ | `max_files` | integer | none | Maximum number of files (only for `multiple: true`) |
433
+ | `variants` | hash | none | Named image variant configurations |
434
+
435
+ ```yaml
436
+ fields:
437
+ # Single image attachment with variants
438
+ - name: photo
439
+ type: attachment
440
+ label: "Photo"
441
+ options:
442
+ accept: "image/*"
443
+ max_size: 5MB
444
+ content_types: ["image/jpeg", "image/png", "image/webp"]
445
+ variants:
446
+ thumbnail: { resize_to_limit: [100, 100] }
447
+ medium: { resize_to_limit: [300, 300] }
448
+
449
+ # Multiple file attachment
450
+ - name: files
451
+ type: attachment
452
+ label: "Documents"
453
+ options:
454
+ multiple: true
455
+ max_files: 10
456
+ max_size: 50MB
457
+ content_types: ["application/pdf", "image/*"]
458
+ ```
459
+
460
+ Attachment fields require Active Storage to be set up in the host application. See the [Attachments Guide](../guides/attachments.md) for prerequisites and complete examples.
461
+
462
+ ##### Array Fields
463
+
464
+ The `array` type stores a list of scalar values. It requires an `item_type` to specify the element type.
465
+
466
+ **Supported item types:** `string`, `integer`, `float`
467
+
468
+ **Storage:** On PostgreSQL, uses native array columns (`text[]`, `integer[]`, `float[]`). On SQLite and other adapters, uses a JSON column with transparent serialization.
469
+
470
+ ```yaml
471
+ fields:
472
+ # String array (e.g., tags)
473
+ - name: tags
474
+ type: array
475
+ item_type: string
476
+ default: []
477
+
478
+ # Integer array (e.g., scores)
479
+ - name: scores
480
+ type: array
481
+ item_type: integer
482
+ default: []
483
+
484
+ # Float array (e.g., measurements)
485
+ - name: measurements
486
+ type: array
487
+ item_type: float
488
+ ```
489
+
490
+ **Auto-generated scopes:** Each array field automatically creates two scopes:
491
+
492
+ | Scope | Description | SQL (PostgreSQL) |
493
+ |-------|-------------|------------------|
494
+ | `with_<field>(values)` | Records whose array contains ALL given values | `@>` (array containment) |
495
+ | `with_any_<field>(values)` | Records whose array contains ANY of the given values | `&&` (array overlap) |
496
+
497
+ ```ruby
498
+ # Find records tagged with both "ruby" AND "rails"
499
+ Article.with_tags(["ruby", "rails"])
500
+
501
+ # Find records tagged with "ruby" OR "python"
502
+ Article.with_any_tags(["ruby", "python"])
503
+ ```
504
+
505
+ **Quick search:** Array fields with `item_type: string` are automatically included in quick search (`?qs=`). The search matches any element that contains the query text (case-insensitive on PostgreSQL, case-sensitive on SQLite).
506
+
507
+ **Presenter defaults:** Array fields automatically use the `array_input` form input (tag-style chips) and the `collection` renderer. These can be overridden in the presenter.
508
+
509
+ See also: [Array validations](#array_length), [Condition operators for arrays](condition-operators.md#array-operators).
510
+
511
+ #### `label`
512
+
513
+ | | |
514
+ |---|---|
515
+ | **Required** | no |
516
+ | **Default** | `name.humanize` |
517
+ | **Type** | string |
518
+
519
+ Display label for form inputs, table headers, and show fields.
520
+
521
+ #### `default`
522
+
523
+ | | |
524
+ |---|---|
525
+ | **Required** | no |
526
+ | **Default** | `nil` |
527
+ | **Type** | scalar, string (built-in key), or hash (service) |
528
+
529
+ Default value for new records. Supports exactly three forms — pick by the type of value you write:
530
+
531
+ | Form | YAML shape | When applied | Use for |
532
+ |------|-----------|--------------|---------|
533
+ | Scalar | `default: false` / `default: "medium"` / `default: 0` | DB column default at table creation | Static literal values |
534
+ | Built-in dynamic | `default: current_date` (string matching a built-in key) | Runtime `after_initialize` on new records | `current_date`, `current_datetime`, `current_year`, `current_user.id` |
535
+ | Service dynamic | `default: { service: "my_service" }` | Runtime `after_initialize` on new records | Custom logic registered in `app/lcp_services/defaults/` |
536
+
537
+ A bare string that does **not** match a built-in key is treated as a literal scalar (e.g. `default: "draft"`). The built-in dynamic keys follow the [Dynamic References](dynamic-references.md) grammar — boot-time validation catches typos against the configured `user_class`. For other dynamic computations expressible as field references see [`computed`](#computed) — distinct from `default` because it re-evaluates on every read.
538
+
539
+ **Scalar default** — applied as a database column default:
540
+
541
+ ```yaml
542
+ - name: completed
543
+ type: boolean
544
+ default: false
545
+
546
+ - name: priority
547
+ type: string
548
+ default: "medium"
549
+ ```
550
+
551
+ **Built-in dynamic default** — a string matching a built-in default key. Applied at runtime via `after_initialize` (only on new records, only when the field is blank):
552
+
553
+ | Key | Value | Description |
554
+ |-----|-------|-------------|
555
+ | `current_date` | `Date.today` | Today's date |
556
+ | `current_datetime` | `Time.current` | Current date and time |
557
+ | `current_user.id` | `LcpRuby::Current.user&.id` | Current user's ID |
558
+
559
+ ```yaml
560
+ - name: start_date
561
+ type: date
562
+ default: current_date
563
+ ```
564
+
565
+ **Service dynamic default** — delegates to a registered default service. Applied at runtime via `after_initialize`:
566
+
567
+ ```yaml
568
+ - name: expected_close_date
569
+ type: date
570
+ default:
571
+ service: thirty_days_out
572
+ ```
573
+
574
+ ```ruby
575
+ # DSL
576
+ field :start_date, :date, default: "current_date"
577
+ field :expected_close_date, :date, default: { service: "thirty_days_out" }
578
+ ```
579
+
580
+ **Defaults on service-accessor fields** — fields whose value lives behind an accessor service (most commonly `source: { service: json_field, ... }`) follow the same Rails-like default semantics as regular columns: the default is applied **on new-record creation** in the `after_initialize` callback, which calls the accessor's setter (e.g. `JsonField.set`) to write the value into the underlying store (JSON column) before `save`. Read-time fallback is NOT used — the accessor's getter returns the raw stored value.
581
+
582
+ ```yaml
583
+ - name: theme
584
+ type: enum
585
+ default: "auto"
586
+ values: { light: "Light", dark: "Dark", auto: "System" }
587
+ source:
588
+ service: json_field
589
+ options: { column: profile_data, key: theme }
590
+ ```
591
+
592
+ For `record = Preference.new`, `record.theme == "auto"` and after `record.save!` the column holds `profile_data == {"theme" => "auto"}`. All three default forms work identically to regular columns:
593
+
594
+ - literal (`default: "auto"`)
595
+ - built-in service key (`default: "current_date"`)
596
+ - service hash (`default: { service: "thirty_days_out" }`)
597
+
598
+ **Bind_to models** get the same behavior automatically — `DefaultApplicator` is in the `bind_to_auto_features` list when any field has a `default:`, so it registers the `after_initialize` callback on the host class. No `bind_to_apply:` declaration is needed.
599
+
600
+ **Existing rows:** If you add a `default:` to a field that already has data in the database, **existing rows with a missing JSON key return `nil`** — the default is not retroactively applied, mirroring how DB column defaults behave in Rails. To backfill old rows, write a one-shot Rails migration:
601
+
602
+ ```ruby
603
+ # db/migrate/20260521_backfill_theme_default.rb
604
+ class BackfillThemeDefault < ActiveRecord::Migration[7.1]
605
+ def up
606
+ LcpRuby::Dynamic::Preference.where("profile_data->>'theme' IS NULL").find_each do |p|
607
+ p.update!(theme: "auto")
608
+ end
609
+ end
610
+ end
611
+ ```
612
+
613
+ The accessor's setter writes the value into the JSON column like any normal update.
614
+
615
+ Service contract: `def self.call(record, field_name) -> value`. Register in `app/lcp_services/defaults/`. See [Extensibility Guide](../guides/extensibility.md).
616
+
617
+ #### `column_options`
618
+
619
+ | | |
620
+ |---|---|
621
+ | **Required** | no |
622
+ | **Default** | `{}` |
623
+ | **Type** | hash |
624
+
625
+ Options passed directly to the ActiveRecord migration column definition.
626
+
627
+ | Option | Applicable Types | Description |
628
+ |--------|-----------------|-------------|
629
+ | `limit` | string | Maximum character length |
630
+ | `precision` | decimal | Total number of digits |
631
+ | `scale` | decimal | Digits after decimal point |
632
+ | `null` | all | Allow NULL values (`true`/`false`) |
633
+
634
+ ```yaml
635
+ column_options:
636
+ limit: 255
637
+ "null": false
638
+ ```
639
+
640
+ > Note: Quote `null` as `"null"` in YAML to avoid it being parsed as a null value.
641
+
642
+ #### `enum_values`
643
+
644
+ | | |
645
+ |---|---|
646
+ | **Required** | yes (when type is `enum`) |
647
+ | **Type** | array of strings or hashes |
648
+
649
+ Defines the allowed values for an enum field. Two formats are supported:
650
+
651
+ **Simple format** — value and label are the same:
652
+
653
+ ```yaml
654
+ enum_values: [draft, published, archived]
655
+ ```
656
+
657
+ **Hash format** — separate internal value and display label:
658
+
659
+ ```yaml
660
+ enum_values:
661
+ - { value: draft, label: "Draft" }
662
+ - { value: published, label: "Published" }
663
+ - { value: archived, label: "Archived" }
664
+ ```
665
+
666
+ Enum values are always stored and compared as strings internally. Even if you write `value: 0` (integer) in YAML, it is converted to `"0"` via `.to_s`.
667
+
668
+ **Display label resolution** (in badges, table cells, show pages, and form selects):
669
+
670
+ 1. **i18n** — `I18n.t("lcp_ruby.enums.<model>.<field>.<value>")` (highest priority)
671
+ 2. **YAML label** — the `label` key from the hash format above
672
+ 3. **Humanize** — `value.humanize` as fallback
673
+
674
+ **Form select ordering**: by default, form `<select>` and radio inputs render enum values in **declaration order** (the order in `enum_values`). To sort alphabetically by label or reverse declaration order, set `input_options: { sort: alphabetical }` (or `reverse`) on the form field — see [Presenters reference — Select (enum)](presenters.md#input-options).
675
+
676
+ #### `item_type`
677
+
678
+ | | |
679
+ |---|---|
680
+ | **Required** | yes (when type is `array`) |
681
+ | **Type** | string |
682
+
683
+ Specifies the scalar type of array elements. Must be one of: `string`, `integer`, `float`.
684
+
685
+ ```yaml
686
+ - name: tags
687
+ type: array
688
+ item_type: string
689
+ ```
690
+
691
+ ```ruby
692
+ # DSL
693
+ field :tags, :array, item_type: :string
694
+ ```
695
+
696
+ #### `transforms`
697
+
698
+ | | |
699
+ |---|---|
700
+ | **Required** | no |
701
+ | **Default** | `[]` |
702
+ | **Type** | array of strings |
703
+
704
+ Field-level transforms applied via `ActiveRecord.normalizes`. Transforms run before validation on every assignment. Field-level transforms extend type-level transforms (defined via [custom types](types.md)) — the union is applied, with type-level transforms running first.
705
+
706
+ ```yaml
707
+ # YAML
708
+ - name: title
709
+ type: string
710
+ transforms: [strip]
711
+
712
+ # With custom type that already has transforms
713
+ - name: email
714
+ type: email # email type has strip+downcase
715
+ transforms: [strip] # deduplicated, won't strip twice
716
+ ```
717
+
718
+ ```ruby
719
+ # DSL
720
+ field :first_name, :string, transforms: [:strip, :titlecase]
721
+ ```
722
+
723
+ Built-in transforms: `strip`, `downcase`, `normalize_url`, `normalize_phone`. Custom transforms can be registered in `app/lcp_services/transforms/`. See [Extensibility Guide](../guides/extensibility.md).
724
+
725
+ #### `computed`
726
+
727
+ | | |
728
+ |---|---|
729
+ | **Required** | no |
730
+ | **Default** | `nil` |
731
+ | **Type** | string (template) or hash (service) |
732
+
733
+ Computed fields are automatically calculated before save. They are rendered as readonly in forms.
734
+
735
+ **Template syntax** — interpolates other field values:
736
+
737
+ ```yaml
738
+ - name: full_name
739
+ type: string
740
+ computed: "{first_name} {last_name}"
741
+ ```
742
+
743
+ The `{field}` micro-syntax (curly braces, dot-paths like `{company.name}`) is identical to the one used by [Display Templates](#display-templates) — the same parser handles both. Use `computed:` when the result is persisted on the record (read-write field, available to scopes and exports); use a display template when you only need it for rendering.
744
+
745
+ **Service syntax** — delegates to a registered computed service:
746
+
747
+ ```yaml
748
+ - name: weighted_value
749
+ type: decimal
750
+ computed:
751
+ service: weighted_deal_value
752
+ ```
753
+
754
+ ```ruby
755
+ # DSL
756
+ field :full_name, :string, computed: "{first_name} {last_name}"
757
+ field :weighted_value, :decimal, computed: { service: "weighted_deal_value" }
758
+ ```
759
+
760
+ Service contract: `def self.call(record) -> value`. Register in `app/lcp_services/computed/`. See [Extensibility Guide](../guides/extensibility.md).
761
+
762
+ **Execution order:** Computed fields are recalculated in the order they appear in the `fields:` list. A computed field can read another computed field's value as long as the dependency is defined earlier. Circular dependencies are not detected — avoiding them is the implementor's responsibility.
763
+
764
+ #### `sequence`
765
+
766
+ | | |
767
+ |---|---|
768
+ | **Required** | no |
769
+ | **Default** | `nil` |
770
+ | **Type** | boolean (`true`) or hash |
771
+
772
+ Auto-numbering sequence field. Assigns a unique sequential value from a gap-free counter on record creation. Requires the `gapfree_sequence` model (run `rails generate lcp_ruby:gapfree_sequences`).
773
+
774
+ **Shorthand** — all defaults (global scope, raw counter, start 1, step 1, readonly):
775
+
776
+ ```yaml
777
+ - name: seq
778
+ type: integer
779
+ sequence: true
780
+ ```
781
+
782
+ **Full form:**
783
+
784
+ ```yaml
785
+ - name: invoice_number
786
+ type: string
787
+ sequence:
788
+ scope: [_year]
789
+ format: "INV-%{_year}-%{sequence:04d}"
790
+ start: 1
791
+ step: 1
792
+ readonly: true
793
+ assign_on: create
794
+ ```
795
+
796
+ | Option | Type | Default | Description |
797
+ |--------|------|---------|-------------|
798
+ | `scope` | array of strings | `[]` (global) | Scope columns or virtual keys (`_year`, `_month`, `_day`). Counter resets per unique scope combination. |
799
+ | `format` | string | none (raw integer) | Template with `%{sequence}`, `%{sequence:Nd}`, `%{_year}`, `%{_month}`, `%{_day}`, `%{field_name}`. |
800
+ | `start` | integer | `1` | Initial counter value for a new scope. |
801
+ | `step` | integer | `1` | Increment per record. |
802
+ | `readonly` | boolean | `true` | Render as disabled input in forms. |
803
+ | `assign_on` | string | `"create"` | `"create"` (before_create only) or `"always"` (also fills blank on update). |
804
+
805
+ ```ruby
806
+ # DSL
807
+ field :invoice_number, :string,
808
+ sequence: { scope: [:_year], format: "INV-%{_year}-%{sequence:04d}" }
809
+ ```
810
+
811
+ A field cannot have both `sequence` and `computed` (or `source`) — these are mutually exclusive. See [Sequence Fields Guide](../guides/sequences.md) for scoping, format templates, and counter management.
812
+
813
+ #### `validations`
814
+
815
+ | | |
816
+ |---|---|
817
+ | **Required** | no |
818
+ | **Default** | `[]` |
819
+ | **Type** | array of validation objects |
820
+
821
+ Field-level validations. See [Validations](#validations) below.
822
+
823
+ ## Validations
824
+
825
+ Validations can be defined at the field level (inside a field's `validations` array) or at the model level (in the top-level `validations` array). Model-level validations support the `custom` and `service` types.
826
+
827
+ ### Validation Types
828
+
829
+ Each validation has a `type` and optional `options` hash.
830
+
831
+ #### `presence`
832
+
833
+ Validates that the field is not blank.
834
+
835
+ ```yaml
836
+ validations:
837
+ - type: presence
838
+ ```
839
+
840
+ > **Not for boolean fields.** `false.present? == false`, so `validates :presence` on a `:boolean` field rejects valid `false` values (a Rails footgun). `bundle exec rake lcp_ruby:validate` reports this as an error and offers three resolutions:
841
+ >
842
+ > - Drop the validator and declare `null: false` on the column. Phase 4b auto-applies `inclusion: { in: [true, false] }` at runtime.
843
+ > - Replace with explicit `validates :inclusion, options: { in: [true, false] }`.
844
+ > - If the field has three meaningful states (yes / no / pending), switch from nullable boolean to `enum` — `null:` boolean tri-state is a data-modeling antipattern (standard checkboxes don't preserve `nil`, `WHERE flag = false` doesn't catch `NULL` rows).
845
+
846
+ With conditional options:
847
+
848
+ ```yaml
849
+ validations:
850
+ - type: presence
851
+ options: { if: "requires_title?" }
852
+ ```
853
+
854
+ #### `length`
855
+
856
+ Validates string length.
857
+
858
+ | Option | Type | Description |
859
+ |--------|------|-------------|
860
+ | `minimum` | integer | Minimum length |
861
+ | `maximum` | integer | Maximum length |
862
+ | `is` | integer | Exact length |
863
+ | `in` | range | Length range (e.g., `3..100`) |
864
+
865
+ ```yaml
866
+ validations:
867
+ - type: length
868
+ options: { minimum: 3, maximum: 100 }
869
+ ```
870
+
871
+ #### `numericality`
872
+
873
+ Validates numeric constraints.
874
+
875
+ | Option | Type | Description |
876
+ |--------|------|-------------|
877
+ | `greater_than` | number | Must be > value |
878
+ | `greater_than_or_equal_to` | number | Must be >= value |
879
+ | `less_than` | number | Must be < value |
880
+ | `less_than_or_equal_to` | number | Must be <= value |
881
+ | `equal_to` | number | Must be exactly value |
882
+ | `allow_nil` | boolean | Skip validation when nil |
883
+
884
+ ```yaml
885
+ validations:
886
+ - type: numericality
887
+ options: { greater_than_or_equal_to: 0, allow_nil: true }
888
+ ```
889
+
890
+ #### `format`
891
+
892
+ Validates against a regular expression.
893
+
894
+ | Option | Type | Description |
895
+ |--------|------|-------------|
896
+ | `with` | string | Regex pattern (converted to `Regexp` at runtime) |
897
+
898
+ ```yaml
899
+ validations:
900
+ - type: format
901
+ options: { with: "\\A[a-zA-Z0-9]+\\z" }
902
+ ```
903
+
904
+ #### `inclusion`
905
+
906
+ Validates that the value is in a given set.
907
+
908
+ | Option | Type | Description |
909
+ |--------|------|-------------|
910
+ | `in` | array | Allowed values |
911
+
912
+ ```yaml
913
+ validations:
914
+ - type: inclusion
915
+ options: { in: [low, medium, high] }
916
+ ```
917
+
918
+ #### `exclusion`
919
+
920
+ Validates that the value is not in a given set.
921
+
922
+ | Option | Type | Description |
923
+ |--------|------|-------------|
924
+ | `in` | array | Excluded values |
925
+
926
+ ```yaml
927
+ validations:
928
+ - type: exclusion
929
+ options: { in: [banned, restricted] }
930
+ ```
931
+
932
+ #### `uniqueness`
933
+
934
+ Validates uniqueness of the value.
935
+
936
+ | Option | Type | Description |
937
+ |--------|------|-------------|
938
+ | `scope` | string or array | Columns to scope uniqueness to |
939
+ | `case_sensitive` | boolean | Case-sensitive comparison |
940
+
941
+ ```yaml
942
+ validations:
943
+ - type: uniqueness
944
+ options: { scope: company_id, case_sensitive: false }
945
+ ```
946
+
947
+ #### `confirmation`
948
+
949
+ Validates that a `<field>_confirmation` attribute matches the field.
950
+
951
+ ```yaml
952
+ validations:
953
+ - type: confirmation
954
+ ```
955
+
956
+ #### `custom`
957
+
958
+ Delegates to a custom validator class. The class must inherit from `ActiveModel::Validator`.
959
+
960
+ | Attribute | Type | Description |
961
+ |-----------|------|-------------|
962
+ | `validator_class` | string | Fully qualified class name |
963
+
964
+ ```yaml
965
+ # Field-level custom validation
966
+ fields:
967
+ - name: email
968
+ type: string
969
+ validations:
970
+ - type: custom
971
+ validator_class: "EmailFormatValidator"
972
+
973
+ # Model-level custom validation
974
+ validations:
975
+ - type: custom
976
+ validator_class: "BusinessRuleValidator"
977
+ ```
978
+
979
+ #### `comparison`
980
+
981
+ Compares the field value against another field on the same record. Skipped when either value is nil.
982
+
983
+ | Attribute | Type | Description |
984
+ |-----------|------|-------------|
985
+ | `operator` | string | Comparison operator: `gt`, `gte`, `lt`, `lte`, `eq`, `not_eq` |
986
+ | `field_ref` | string | Name of the field to compare against |
987
+ | `message` | string | Custom error message |
988
+
989
+ ```yaml
990
+ fields:
991
+ - name: due_date
992
+ type: date
993
+ validations:
994
+ - type: comparison
995
+ operator: gte
996
+ field_ref: start_date
997
+ message: "must be on or after start date"
998
+ ```
999
+
1000
+ ```ruby
1001
+ # DSL
1002
+ field :due_date, :date do
1003
+ validates :comparison, operator: :gte, field_ref: :start_date,
1004
+ message: "must be on or after start date"
1005
+ end
1006
+ ```
1007
+
1008
+ #### `service`
1009
+
1010
+ Delegates validation to a registered service class. The service receives the record and can add errors.
1011
+
1012
+ | Attribute | Type | Description |
1013
+ |-----------|------|-------------|
1014
+ | `service` | string | Service key registered in `app/lcp_services/validators/` |
1015
+
1016
+ ```yaml
1017
+ # Model-level service validation
1018
+ validations:
1019
+ - type: service
1020
+ service: deal_credit_limit
1021
+ ```
1022
+
1023
+ ```ruby
1024
+ # DSL
1025
+ validates_model :service, service: "deal_credit_limit"
1026
+ ```
1027
+
1028
+ Service contract: `def self.call(record, **opts) -> void` (adds errors directly to `record.errors`). Register in `app/lcp_services/validators/`. See [Extensibility Guide](../guides/extensibility.md).
1029
+
1030
+ #### `array_length`
1031
+
1032
+ Validates the number of items in an array field.
1033
+
1034
+ | Option | Type | Description |
1035
+ |--------|------|-------------|
1036
+ | `minimum` | integer | Minimum number of items |
1037
+ | `maximum` | integer | Maximum number of items |
1038
+
1039
+ ```yaml
1040
+ fields:
1041
+ - name: tags
1042
+ type: array
1043
+ item_type: string
1044
+ validations:
1045
+ - type: array_length
1046
+ options: { minimum: 1, maximum: 10 }
1047
+ ```
1048
+
1049
+ ```ruby
1050
+ # DSL
1051
+ field :tags, :array, item_type: :string do
1052
+ validates :array_length, minimum: 1, maximum: 10
1053
+ end
1054
+ ```
1055
+
1056
+ #### `array_inclusion`
1057
+
1058
+ Validates that all items in the array are within an allowed set.
1059
+
1060
+ | Option | Type | Description |
1061
+ |--------|------|-------------|
1062
+ | `in` | array | Allowed values |
1063
+
1064
+ ```yaml
1065
+ fields:
1066
+ - name: tags
1067
+ type: array
1068
+ item_type: string
1069
+ validations:
1070
+ - type: array_inclusion
1071
+ options: { in: [ruby, python, java, go, rust] }
1072
+ ```
1073
+
1074
+ ```ruby
1075
+ # DSL
1076
+ field :tags, :array, item_type: :string do
1077
+ validates :array_inclusion, in: %w[ruby python java go rust]
1078
+ end
1079
+ ```
1080
+
1081
+ #### `array_uniqueness`
1082
+
1083
+ Validates that all items in the array are unique (no duplicates).
1084
+
1085
+ ```yaml
1086
+ fields:
1087
+ - name: tags
1088
+ type: array
1089
+ item_type: string
1090
+ validations:
1091
+ - type: array_uniqueness
1092
+ ```
1093
+
1094
+ ```ruby
1095
+ # DSL
1096
+ field :tags, :array, item_type: :string do
1097
+ validates :array_uniqueness
1098
+ end
1099
+ ```
1100
+
1101
+ ### Conditional Validations (`when:`)
1102
+
1103
+ Any validation can be made conditional using the `when:` key. When present, the validation only runs if the condition evaluates to true. The condition uses the same `{ field:, operator:, value: }` syntax as [`visible_when`](presenters.md#field-visibility) and [`condition`](condition-operators.md).
1104
+
1105
+ ```yaml
1106
+ fields:
1107
+ - name: value
1108
+ type: decimal
1109
+ validations:
1110
+ - type: presence
1111
+ when:
1112
+ field: stage
1113
+ operator: not_in
1114
+ value: [lead]
1115
+ - type: numericality
1116
+ options: { greater_than_or_equal_to: 0, allow_nil: true }
1117
+ ```
1118
+
1119
+ ```ruby
1120
+ # DSL
1121
+ field :value, :decimal do
1122
+ validates :presence, when: { field: :stage, operator: :not_in, value: %w[lead] }
1123
+ validates :numericality, greater_than_or_equal_to: 0, allow_nil: true
1124
+ end
1125
+ ```
1126
+
1127
+ Service-based conditions are also supported:
1128
+
1129
+ ```yaml
1130
+ validations:
1131
+ - type: presence
1132
+ when:
1133
+ service: requires_approval
1134
+ ```
1135
+
1136
+ ### Common Validation Options
1137
+
1138
+ These options can be added to any validation type:
1139
+
1140
+ | Option | Type | Description |
1141
+ |--------|------|-------------|
1142
+ | `message` | string | Custom error message |
1143
+ | `allow_blank` | boolean | Skip validation when value is blank |
1144
+ | `if` | string | Method name; only validate when method returns true |
1145
+ | `unless` | string | Method name; skip validation when method returns true |
1146
+
1147
+ ```yaml
1148
+ validations:
1149
+ - type: presence
1150
+ options: { message: "must be provided", if: "active?" }
1151
+ - type: numericality
1152
+ options: { greater_than: 0, allow_blank: true, unless: "draft?" }
1153
+ ```
1154
+
1155
+ The `if` and `unless` options reference method names on the model instance. These methods must be defined in a model extension or custom validator.
1156
+
1157
+ ## Associations
1158
+
1159
+ Associations define relationships between models.
1160
+
1161
+ ```yaml
1162
+ associations:
1163
+ # Standard belongs_to
1164
+ - type: belongs_to
1165
+ name: company
1166
+ target_model: company
1167
+ foreign_key: company_id
1168
+ required: true
1169
+ inverse_of: employees
1170
+
1171
+ # Polymorphic belongs_to (creates _id + _type columns automatically)
1172
+ - type: belongs_to
1173
+ name: commentable
1174
+ polymorphic: true
1175
+ required: false
1176
+
1177
+ # has_many with polymorphic inverse
1178
+ - type: has_many
1179
+ name: comments
1180
+ target_model: comment
1181
+ as: commentable
1182
+
1183
+ # has_many through (join model)
1184
+ - type: has_many
1185
+ name: taggings
1186
+ target_model: tagging
1187
+ - type: has_many
1188
+ name: tags
1189
+ through: taggings
1190
+ source: tag
1191
+
1192
+ # Join on a business key instead of the target's primary key.
1193
+ # The owner's `division_code` matches `divisions.vema_code`.
1194
+ - type: belongs_to
1195
+ name: division
1196
+ target_model: division
1197
+ foreign_key: division_code
1198
+ primary_key: vema_code
1199
+ required: false
1200
+ ```
1201
+
1202
+ ### Association Attributes
1203
+
1204
+ #### `type`
1205
+
1206
+ | | |
1207
+ |---|---|
1208
+ | **Required** | yes |
1209
+ | **Allowed** | `belongs_to`, `has_many`, `has_one` |
1210
+
1211
+ The ActiveRecord association type.
1212
+
1213
+ #### `name`
1214
+
1215
+ | | |
1216
+ |---|---|
1217
+ | **Required** | yes |
1218
+ | **Type** | string |
1219
+
1220
+ The association name. Used as the method name on the model (e.g., `record.company`).
1221
+
1222
+ #### `target_model`
1223
+
1224
+ | | |
1225
+ |---|---|
1226
+ | **Required** | conditionally (see below) |
1227
+ | **Type** | string |
1228
+
1229
+ Name of another LCP model. The engine resolves this to `LcpRuby::Dynamic::<TargetModel>` at runtime. Use this for associations between LCP-managed models.
1230
+
1231
+ At least one of `target_model`, `class_name`, `polymorphic`, `as`, or `through` must be present.
1232
+
1233
+ ```yaml
1234
+ - type: belongs_to
1235
+ name: todo_list
1236
+ target_model: todo_list
1237
+ foreign_key: todo_list_id
1238
+ ```
1239
+
1240
+ #### `class_name`
1241
+
1242
+ | | |
1243
+ |---|---|
1244
+ | **Required** | conditionally (see `target_model`) |
1245
+ | **Type** | string |
1246
+
1247
+ Fully qualified class name for associations pointing to non-LCP models (e.g., your host app's `User` model). When `target_model` is set, `class_name` is ignored — the engine generates it automatically.
1248
+
1249
+ ```yaml
1250
+ - type: belongs_to
1251
+ name: author
1252
+ class_name: "User"
1253
+ foreign_key: author_id
1254
+ ```
1255
+
1256
+ #### `foreign_key`
1257
+
1258
+ | | |
1259
+ |---|---|
1260
+ | **Required** | no |
1261
+ | **Default** | Rails convention (`<name>_id` for `belongs_to`) |
1262
+ | **Type** | string |
1263
+
1264
+ The foreign key column name. Specify this when the column name does not follow Rails naming conventions.
1265
+
1266
+ #### `primary_key`
1267
+
1268
+ | | |
1269
+ |---|---|
1270
+ | **Required** | no |
1271
+ | **Default** | target model's primary key column (usually `id`) |
1272
+ | **Type** | string |
1273
+ | **Applies to** | `belongs_to`, `has_many`, `has_one` (not `through:`, not `polymorphic:`) |
1274
+
1275
+ Column on the PK side of the join that the `foreign_key` matches against. Mirrors Rails's native `primary_key:` option and lets associations join on a natural business key (`vema_code`, `iso_code`, `sku`, ...) instead of a surrogate `id`.
1276
+
1277
+ **Semantics — the join pair depends on association type:**
1278
+
1279
+ | Association type (declared on *owner*) | Effective join pair |
1280
+ |---|---|
1281
+ | `belongs_to` | `owner.foreign_key = target.primary_key` |
1282
+ | `has_many` (non-`through`) | `target.foreign_key = owner.primary_key` |
1283
+ | `has_one` (non-`through`) | `target.foreign_key = owner.primary_key` |
1284
+
1285
+ ```yaml
1286
+ # On employee — belongs_to joins to a non-PK column on the target
1287
+ - type: belongs_to
1288
+ name: division
1289
+ target_model: division
1290
+ foreign_key: division_code
1291
+ primary_key: vema_code # employees.division_code = divisions.vema_code
1292
+ required: false
1293
+
1294
+ # On division — reverse side, same join pair
1295
+ - type: has_many
1296
+ name: employees
1297
+ target_model: employee
1298
+ foreign_key: division_code
1299
+ primary_key: vema_code # employees.division_code = divisions.vema_code
1300
+ ```
1301
+
1302
+ **FK column type.** When `primary_key:` points at an LCP model's field, the auto-created FK column on the `belongs_to` side matches that field's type instead of the default `bigint`. For a `belongs_to :division, primary_key: :vema_code` where `divisions.vema_code` is `integer`, the engine creates `employees.division_code` as `integer`. For string business keys (`"EUR"`, `"0042"`), the FK is created as `string`.
1303
+
1304
+ When `primary_key:` points at a non-LCP class (`class_name:` without `target_model:`), LCP cannot introspect the target column's type — the configurator **must** declare the FK column explicitly via `fields:`:
1305
+
1306
+ ```yaml
1307
+ fields:
1308
+ - name: division_code
1309
+ type: string # must match the external column type
1310
+
1311
+ associations:
1312
+ - type: belongs_to
1313
+ name: division
1314
+ class_name: "::HostDivision"
1315
+ foreign_key: division_code
1316
+ primary_key: vema_code
1317
+ ```
1318
+
1319
+ Without the explicit field, `ConfigurationValidator` appends an error at boot.
1320
+
1321
+ **Boot-time validation** (runs via `bundle exec rake lcp_ruby:validate`):
1322
+
1323
+ - The referenced column must exist on the PK side (target for `belongs_to`, owner for `has_many`/`has_one`).
1324
+ - A warning fires when the column has no uniqueness constraint (unscoped `validates … uniqueness: true` or a single-column unique `indexes:` entry). Scoped uniqueness emits a softer warning pointing at `query_constraints:` as the proper fix for partition-scoped joins.
1325
+ - A warning fires when the FK and PK column types differ.
1326
+ - A warning fires when the FK is declared as an explicit `field:` but has no covering `indexes:` entry — auto-indexing does not cover explicit FK fields, so joins would silently degrade to table scans.
1327
+ - A warning fires when the forward and inverse associations declare different join pairs (reciprocity alignment).
1328
+
1329
+ **Rejected configurations** (hard errors):
1330
+
1331
+ - `primary_key:` together with `through:` — put it on the individual link associations instead.
1332
+ - `primary_key:` together with `polymorphic: true` — each polymorphic target can have a different PK column; there is no single column that makes sense.
1333
+ - `primary_key:` on tree-managed self-referencing associations (the recursive CTE hardcodes `id`-based joins — out of scope for v1).
1334
+ - `primary_key:` on cross-source (DB ↔ API) associations — the API applicator has its own `id`-based lookup paths that do not yet read the join column (out of scope for v1).
1335
+
1336
+ **`bind_to:` models.** `primary_key:` is allowed on associations whose target is a `bind_to` model (LCP reads the YAML to render form selects, cascading selects, tree selects, [page filter form fields](page_filters.md), import/export metadata, and virtual-column SQL). The host AR class is responsible for declaring its own Rails macro-level `primary_key:`. Uniqueness and type-compat checks are skipped for `bind_to` targets because LCP cannot introspect the host schema — to opt back into the column-exists check, list the join column in the `bind_to` model's YAML `fields:`.
1337
+
1338
+ **Migration note — redundant denormalized columns.** The common motivation for this feature is dropping denormalized columns (`division_code` + `division_name` + `bu_code` + `bu_name`) in favor of a single FK that joins on the business code. The migration steps are:
1339
+
1340
+ 1. Add `primary_key:` to the association in YAML.
1341
+ 2. Remove the redundant field entries (`division_name`, `bu_code`, `bu_name`, ...) from the owner model.
1342
+ 3. Update any host code that referenced the redundant fields to traverse via the association (`record.division.vema_name`, `record.division.business_unit.vema_name`, ...).
1343
+
1344
+ LCP's auto-migration is additive — removing a YAML field leaves the column in the DB. A manual SQL migration to drop the column is optional and host-app-specific.
1345
+
1346
+ See [design/association_primary_key.md](../design/association_primary_key.md) for the full feature specification, including every downstream consumer that was updated (form helpers, cascading selects, tree selects, page parameters [retired in PR 5; replaced by [filter forms](page_filters.md)], import/export field trees, virtual columns).
1347
+
1348
+ #### `dependent`
1349
+
1350
+ | | |
1351
+ |---|---|
1352
+ | **Required** | no |
1353
+ | **Default** | none |
1354
+ | **Type** | string |
1355
+ | **Allowed** | `destroy`, `delete`, `delete_all`, `nullify`, `restrict_with_error`, `restrict_with_exception`, `discard` |
1356
+
1357
+ What happens to associated records when the parent is destroyed (or discarded). Applicable to all association types.
1358
+
1359
+ The `discard` value is special — it is **not** passed to ActiveRecord. Instead, when the parent record is discarded (soft-deleted), the `SoftDeleteApplicator` cascades the discard to child records with `dependent: :discard`. This requires:
1360
+ - The parent model must have `soft_delete` enabled
1361
+ - The target (child) model must also have `soft_delete` enabled
1362
+ - Only valid on `has_many` / `has_one` associations (not `belongs_to`)
1363
+
1364
+ #### `required`
1365
+
1366
+ | | |
1367
+ |---|---|
1368
+ | **Required** | no (but **strongly recommended on `belongs_to`** — see note) |
1369
+ | **Default** | `true` for `belongs_to` (AR Rails 5+), `false` for `has_many`/`has_one` |
1370
+ | **Type** | boolean |
1371
+
1372
+ Whether the association is mandatory. For `belongs_to`, setting `required: false` allows the foreign key to be NULL.
1373
+
1374
+ > **Always set `required:` explicitly on `belongs_to`.** Omitting it inherits the AR Rails 5+ default (`required: true`), which silently rejects nullable parents at save time. `bundle exec rake lcp_ruby:validate` warns whenever a `belongs_to` declaration lacks an explicit `required:` kwarg. The `lcp_ruby:entity` generator emits it for every generated association.
1375
+
1376
+ #### `inverse_of`
1377
+
1378
+ | | |
1379
+ |---|---|
1380
+ | **Required** | no |
1381
+ | **Default** | none (Rails auto-detection) |
1382
+ | **Type** | string |
1383
+ | **Applies to** | all types |
1384
+
1385
+ Specifies the inverse association on the target model. Rails usually infers this, but explicit setting avoids ambiguity and improves performance by preventing extra queries.
1386
+
1387
+ ```yaml
1388
+ associations:
1389
+ - type: has_many
1390
+ name: tasks
1391
+ target_model: task
1392
+ inverse_of: project
1393
+ ```
1394
+
1395
+ #### `counter_cache`
1396
+
1397
+ | | |
1398
+ |---|---|
1399
+ | **Required** | no |
1400
+ | **Default** | none |
1401
+ | **Type** | boolean or string |
1402
+ | **Applies to** | `belongs_to` |
1403
+
1404
+ Maintains a count column on the parent model. Set to `true` to use the default column name (`<children>_count`), or a string for a custom column name.
1405
+
1406
+ The counter column must be added as a field on the parent model.
1407
+
1408
+ ```yaml
1409
+ # On child model
1410
+ associations:
1411
+ - type: belongs_to
1412
+ name: project
1413
+ target_model: project
1414
+ counter_cache: true
1415
+
1416
+ # Or with custom column name
1417
+ counter_cache: tasks_count
1418
+ ```
1419
+
1420
+ #### `touch`
1421
+
1422
+ | | |
1423
+ |---|---|
1424
+ | **Required** | no |
1425
+ | **Default** | none |
1426
+ | **Type** | boolean or string |
1427
+ | **Applies to** | `belongs_to` |
1428
+
1429
+ Updates the parent's `updated_at` timestamp when the child is saved. Set to `true` for `updated_at`, or a string for a custom timestamp column.
1430
+
1431
+ ```yaml
1432
+ associations:
1433
+ - type: belongs_to
1434
+ name: project
1435
+ target_model: project
1436
+ touch: true
1437
+ ```
1438
+
1439
+ #### `polymorphic`
1440
+
1441
+ | | |
1442
+ |---|---|
1443
+ | **Required** | no |
1444
+ | **Default** | `false` |
1445
+ | **Type** | boolean |
1446
+ | **Applies to** | `belongs_to` |
1447
+
1448
+ Creates a polymorphic association. When `true`, the engine automatically creates both `<name>_id` and `<name>_type` columns. No `target_model` or `class_name` is needed — the type is determined at runtime from the `_type` column.
1449
+
1450
+ ```yaml
1451
+ # On child model (e.g., comment)
1452
+ associations:
1453
+ - type: belongs_to
1454
+ name: commentable
1455
+ polymorphic: true
1456
+ required: false
1457
+ ```
1458
+
1459
+ #### `as`
1460
+
1461
+ | | |
1462
+ |---|---|
1463
+ | **Required** | no |
1464
+ | **Default** | none |
1465
+ | **Type** | string |
1466
+ | **Applies to** | `has_many`, `has_one` |
1467
+
1468
+ The polymorphic interface name on the target model. Used with `polymorphic` belongs_to on the other side.
1469
+
1470
+ ```yaml
1471
+ # On parent model (e.g., post)
1472
+ associations:
1473
+ - type: has_many
1474
+ name: comments
1475
+ target_model: comment
1476
+ as: commentable
1477
+ ```
1478
+
1479
+ #### `through`
1480
+
1481
+ | | |
1482
+ |---|---|
1483
+ | **Required** | no |
1484
+ | **Default** | none |
1485
+ | **Type** | string |
1486
+ | **Applies to** | `has_many`, `has_one` |
1487
+
1488
+ Creates a through association via a join model. The value is the name of another association on this model that serves as the join. No FK columns are created for through associations.
1489
+
1490
+ ```yaml
1491
+ associations:
1492
+ - type: has_many
1493
+ name: taggings
1494
+ target_model: tagging
1495
+ - type: has_many
1496
+ name: tags
1497
+ through: taggings
1498
+ ```
1499
+
1500
+ #### `source`
1501
+
1502
+ | | |
1503
+ |---|---|
1504
+ | **Required** | no |
1505
+ | **Default** | none (Rails infers from association name) |
1506
+ | **Type** | string |
1507
+ | **Applies to** | `has_many`, `has_one` (with `through`) |
1508
+
1509
+ Specifies the source association on the join model. Only needed when the name cannot be inferred automatically.
1510
+
1511
+ ```yaml
1512
+ associations:
1513
+ - type: has_many
1514
+ name: authors
1515
+ through: authorships
1516
+ source: person
1517
+ ```
1518
+
1519
+ #### `autosave`
1520
+
1521
+ | | |
1522
+ |---|---|
1523
+ | **Required** | no |
1524
+ | **Default** | none (Rails default) |
1525
+ | **Type** | boolean |
1526
+ | **Applies to** | all types |
1527
+
1528
+ When `true`, saves associated records whenever the parent is saved.
1529
+
1530
+ ```yaml
1531
+ associations:
1532
+ - type: has_many
1533
+ name: items
1534
+ target_model: item
1535
+ autosave: true
1536
+ ```
1537
+
1538
+ #### `validate`
1539
+
1540
+ | | |
1541
+ |---|---|
1542
+ | **Required** | no |
1543
+ | **Default** | none (Rails default) |
1544
+ | **Type** | boolean |
1545
+ | **Applies to** | all types |
1546
+
1547
+ When `true`, validates associated records on save. Set to `false` to skip validation of associated records.
1548
+
1549
+ ```yaml
1550
+ associations:
1551
+ - type: has_many
1552
+ name: items
1553
+ target_model: item
1554
+ validate: false
1555
+ ```
1556
+
1557
+ #### `order`
1558
+
1559
+ | | |
1560
+ |---|---|
1561
+ | **Required** | no |
1562
+ | **Default** | none |
1563
+ | **Type** | hash |
1564
+ | **Applies to** | `has_many`, `has_one` |
1565
+
1566
+ Default ordering for associated records. The engine passes this as a scope lambda to the ActiveRecord association. Keys are column names, values are `asc` or `desc`.
1567
+
1568
+ ```yaml
1569
+ associations:
1570
+ - type: has_many
1571
+ name: todo_items
1572
+ target_model: todo_item
1573
+ order:
1574
+ position: asc
1575
+ ```
1576
+
1577
+ This generates: `has_many :todo_items, -> { order(position: :asc) }, ...`
1578
+
1579
+ Useful for sortable nested forms where child records have a position field.
1580
+
1581
+ #### `skip_inverse_check`
1582
+
1583
+ | | |
1584
+ |---|---|
1585
+ | **Required** | no |
1586
+ | **Default** | `false` |
1587
+ | **Type** | boolean |
1588
+ | **Applies to** | `belongs_to` only |
1589
+
1590
+ Suppresses the missing-inverse warning from `lcp_ruby:validate`. Use when a `belongs_to` is intentionally one-directional and you don't want a `has_many` on the parent side.
1591
+
1592
+ **When to use:** audit/owner/author references where reverse traversal would be noise.
1593
+
1594
+ ```yaml
1595
+ associations:
1596
+ - type: belongs_to
1597
+ name: author
1598
+ target_model: teacher
1599
+ skip_inverse_check: true # `teacher.has_many :authored_announcements` would be unused noise
1600
+ - type: belongs_to
1601
+ name: created_by
1602
+ target_model: user
1603
+ skip_inverse_check: true # `user.has_many :created_things` × N tables: clutter
1604
+ ```
1605
+
1606
+ **When NOT to use:** the inverse would actually be queried (`teacher.school_classes`, `school_class.students`). Adding the `has_many` is the right move; this flag exists for the cases where it isn't.
1607
+
1608
+ **Validation.** `skip_inverse_check: true` on `has_many` or `has_one` raises `MetadataError` at boot — that case (a parent declaring children with no `belongs_to` back) is a genuine bug because the FK has nowhere to go.
1609
+
1610
+ ## Nested Attributes
1611
+
1612
+ Associations can declare `nested_attributes` to enable creating and updating associated records through the parent model's form and the import system. This uses Rails' `accepts_nested_attributes_for` under the hood.
1613
+
1614
+ ### Boolean Shorthand
1615
+
1616
+ For `has_one` associations, the recommended form is the boolean shorthand:
1617
+
1618
+ ```yaml
1619
+ associations:
1620
+ - type: has_one
1621
+ name: employee_profile
1622
+ target_model: employee_profile
1623
+ dependent: destroy
1624
+ inverse_of: employee
1625
+ nested_attributes: true # safe defaults: reject_if: all_blank, update_only: true
1626
+ ```
1627
+
1628
+ `nested_attributes: true` is equivalent to `{ reject_if: all_blank, update_only: true }`. These defaults are safe for both forms and import:
1629
+ - `reject_if: all_blank` prevents creating empty nested records when all fields are blank
1630
+ - `update_only: true` updates existing nested records in-place instead of destroying and recreating them (which would break foreign key references)
1631
+
1632
+ ### Explicit Hash Form
1633
+
1634
+ For fine-grained control, use the explicit hash:
1635
+
1636
+ ```yaml
1637
+ associations:
1638
+ - type: has_many
1639
+ name: todo_items
1640
+ target_model: todo_item
1641
+ dependent: destroy
1642
+ inverse_of: todo_list
1643
+ nested_attributes:
1644
+ allow_destroy: true
1645
+ reject_if: all_blank
1646
+ limit: 50
1647
+ update_only: false
1648
+ ```
1649
+
1650
+ ### Nested Attributes Keys
1651
+
1652
+ #### `allow_destroy`
1653
+
1654
+ | | |
1655
+ |---|---|
1656
+ | **Required** | no |
1657
+ | **Default** | `false` |
1658
+ | **Type** | boolean |
1659
+
1660
+ When `true`, nested records can be marked for deletion by passing `_destroy: true` in the nested attributes hash. Without this option, nested records can only be created and updated, not removed through the parent form.
1661
+
1662
+ ```yaml
1663
+ nested_attributes:
1664
+ allow_destroy: true
1665
+ ```
1666
+
1667
+ #### `reject_if`
1668
+
1669
+ | | |
1670
+ |---|---|
1671
+ | **Required** | no |
1672
+ | **Default** | none |
1673
+ | **Type** | string |
1674
+
1675
+ Controls when nested records are silently rejected (not saved). The special value `"all_blank"` rejects any nested record where every attribute value is blank. This is useful for forms that render empty rows for new records — blank rows are ignored instead of causing validation errors.
1676
+
1677
+ Can also be set to a symbol name referencing a custom method on the model that returns `true` to reject the record.
1678
+
1679
+ ```yaml
1680
+ nested_attributes:
1681
+ reject_if: all_blank
1682
+ ```
1683
+
1684
+ #### `limit`
1685
+
1686
+ | | |
1687
+ |---|---|
1688
+ | **Required** | no |
1689
+ | **Default** | none (unlimited) |
1690
+ | **Type** | integer |
1691
+
1692
+ Maximum number of nested records that can be processed at once. If the incoming attributes hash contains more records than the limit, a `TooManyRecords` exception is raised. Use this to prevent abuse or accidental mass-creation of associated records.
1693
+
1694
+ ```yaml
1695
+ nested_attributes:
1696
+ limit: 50
1697
+ ```
1698
+
1699
+ #### `update_only`
1700
+
1701
+ | | |
1702
+ |---|---|
1703
+ | **Required** | no |
1704
+ | **Default** | `false` |
1705
+ | **Type** | boolean |
1706
+
1707
+ When `true`, existing nested records are updated in-place instead of being destroyed and recreated. This preserves the nested record's ID, which is important when other records reference it via foreign keys.
1708
+
1709
+ **Note:** Despite the name, `update_only: true` still allows **creating** new nested records when none exists. Rails' internal logic falls through to `build_<association>` when no existing record is found. The name only means "don't destroy+recreate existing records".
1710
+
1711
+ ```yaml
1712
+ nested_attributes:
1713
+ update_only: true
1714
+ ```
1715
+
1716
+ ### Requirements
1717
+
1718
+ - The parent association **must** specify `inverse_of` for nested attributes to work correctly. Without `inverse_of`, Rails cannot properly link the parent and child records during validation, which can cause unexpected behavior or validation failures.
1719
+ - Nested attributes are used with `has_many` and `has_one` associations.
1720
+ - `has_one :through` associations are **not compatible** with `nested_attributes` (no direct FK). The `ConfigurationValidator` warns at boot if this is attempted.
1721
+
1722
+ ### Import Integration
1723
+
1724
+ `has_one` associations with `nested_attributes` enabled are automatically available for import. See [Import Reference](import.md#nested-has_one-import) for details on:
1725
+ - Dot-notation column mapping (`hire_detail.starting_salary`)
1726
+ - `nested_associations` whitelist on the presenter
1727
+ - `nested_blank_strategy` for controlling blank value handling
1728
+
1729
+ ### Full Example
1730
+
1731
+ ```yaml
1732
+ model:
1733
+ name: todo_list
1734
+ label: "Todo List"
1735
+
1736
+ fields:
1737
+ - name: name
1738
+ type: string
1739
+ validations:
1740
+ - type: presence
1741
+
1742
+ associations:
1743
+ - type: has_many
1744
+ name: todo_items
1745
+ target_model: todo_item
1746
+ dependent: destroy
1747
+ inverse_of: todo_list
1748
+ nested_attributes:
1749
+ allow_destroy: true
1750
+ reject_if: all_blank
1751
+ limit: 50
1752
+ ```
1753
+
1754
+ ## Scopes
1755
+
1756
+ Scopes define named query methods on the model.
1757
+
1758
+ ```yaml
1759
+ scopes:
1760
+ - name: open_deals
1761
+ where_not: { stage: [closed_won, closed_lost] }
1762
+ - name: won
1763
+ where: { stage: closed_won }
1764
+ - name: recent # order + limit only — no where
1765
+ order: { created_at: desc }
1766
+ limit: 10
1767
+ - name: upcoming # order-only sort helper
1768
+ order: { due_at: asc }
1769
+ - name: first_five # limit-only cap
1770
+ limit: 5
1771
+ ```
1772
+
1773
+ Only `name` is required — every other attribute (`where`, `where_not`, `order`, `limit`) is independently optional. Combine any subset, or use just one. Do not write `where: {}` to satisfy a perceived requirement; just omit it.
1774
+
1775
+ ### Scope Attributes
1776
+
1777
+ #### `name`
1778
+
1779
+ | | |
1780
+ |---|---|
1781
+ | **Required** | yes |
1782
+ | **Type** | string |
1783
+
1784
+ The scope method name. Called as `ModelClass.scope_name` in queries and referenced from [predefined filters](presenters.md#search-configuration).
1785
+
1786
+ #### `where`
1787
+
1788
+ | | |
1789
+ |---|---|
1790
+ | **Required** | no |
1791
+ | **Type** | hash |
1792
+
1793
+ Generates `scope :name, -> { where(...) }`. Keys are column names, values are the expected values (scalar or array).
1794
+
1795
+ #### `where_not`
1796
+
1797
+ | | |
1798
+ |---|---|
1799
+ | **Required** | no |
1800
+ | **Type** | hash |
1801
+
1802
+ Generates `scope :name, -> { where.not(...) }`. Same syntax as `where` but negated.
1803
+
1804
+ #### `order`
1805
+
1806
+ | | |
1807
+ |---|---|
1808
+ | **Required** | no |
1809
+ | **Type** | hash |
1810
+
1811
+ Generates ordering. Keys are column names, values are `asc` or `desc`.
1812
+
1813
+ Valid column names are everything `SchemaManager` puts on the table: declared `field:` rows, `belongs_to` FK columns (incl. polymorphic `_type`), userstamps, timestamps when enabled, soft-delete column, positioning column, and `id`. The same set is accepted in `where:` / `where_not:`.
1814
+
1815
+ #### `limit`
1816
+
1817
+ | | |
1818
+ |---|---|
1819
+ | **Required** | no |
1820
+ | **Type** | integer |
1821
+
1822
+ Limits the number of returned records. Combine with `order` for "top N" queries.
1823
+
1824
+ #### `type`
1825
+
1826
+ | | |
1827
+ |---|---|
1828
+ | **Required** | no |
1829
+ | **Allowed** | `custom`, `parameterized` |
1830
+
1831
+ - `custom` — the scope is not generated from YAML. Instead, it must be defined in Ruby code via a model extension. The scope entry in YAML serves as documentation and allows it to be referenced from predefined filters.
1832
+ - `parameterized` — the scope accepts typed parameters at runtime. The `parameters` attribute defines the parameter schema (see below). The actual scope must be defined in Ruby — either as a standard AR scope with keyword arguments, or as a `filter_*` interceptor method.
1833
+
1834
+ #### `parameters`
1835
+
1836
+ | | |
1837
+ |---|---|
1838
+ | **Required** | only when `type: parameterized` |
1839
+ | **Type** | array of parameter definitions |
1840
+
1841
+ Each parameter has:
1842
+
1843
+ | Key | Type | Required | Description |
1844
+ |-----|------|----------|-------------|
1845
+ | `name` | string | yes | Parameter name (used as keyword argument) |
1846
+ | `type` | string | yes | One of: `boolean`, `string`, `integer`, `float`, `enum`, `date`, `datetime`, `model_select` |
1847
+ | `default` | any | no | Default value when the parameter is not provided |
1848
+ | `required` | boolean | no | Whether the parameter must be provided (default: `false`). If a required parameter is missing, the scope is skipped |
1849
+ | `min` | number | no | Minimum value (integer/float only). Values below this are clamped |
1850
+ | `max` | number | no | Maximum value (integer/float only). Values above this are clamped |
1851
+ | `values` | array | no | Allowed values (enum type only). Values not in the list are rejected |
1852
+ | `model` | string | no | Target model name (model_select only) |
1853
+ | `display_field` | string | no | Field to display in the select dropdown (model_select only) |
1854
+
1855
+ ### Parameterized Scopes
1856
+
1857
+ Parameterized scopes let users configure scope arguments at runtime. Parameters are cast, validated, and clamped before being passed to the scope.
1858
+
1859
+ ```yaml
1860
+ scopes:
1861
+ - name: created_recently
1862
+ type: parameterized
1863
+ parameters:
1864
+ - name: days
1865
+ type: integer
1866
+ default: 30
1867
+ min: 1
1868
+ max: 365
1869
+
1870
+ - name: by_min_price
1871
+ type: parameterized
1872
+ parameters:
1873
+ - name: min_price
1874
+ type: float
1875
+ default: 0.0
1876
+ min: 0
1877
+
1878
+ - name: by_status_filter
1879
+ type: parameterized
1880
+ parameters:
1881
+ - name: status
1882
+ type: enum
1883
+ values: [draft, published, archived]
1884
+ required: true
1885
+ ```
1886
+
1887
+ The Ruby implementation can be either a standard scope or a `filter_*` interceptor:
1888
+
1889
+ ```ruby
1890
+ # Option A: AR scope with keyword arguments
1891
+ scope :created_recently, ->(days: 30) {
1892
+ where("created_at >= ?", days.to_i.days.ago)
1893
+ }
1894
+
1895
+ # Option B: filter_* interceptor (more flexible)
1896
+ def self.filter_by_min_price(scope, params, evaluator)
1897
+ min = params[:min_price] || 0
1898
+ scope.where("price >= ?", min)
1899
+ end
1900
+ ```
1901
+
1902
+ In the [query language](../design/advanced_search.md), parameterized scopes use `@` prefix syntax:
1903
+
1904
+ ```
1905
+ @created_recently(days: 7)
1906
+ @by_status_filter(status: 'published')
1907
+ ```
1908
+
1909
+ ### Combined Scopes
1910
+
1911
+ Scope attributes are additive — a single scope can combine `where`, `where_not`, `order`, and `limit`. They are also independent — any subset is valid (just `order:`, just `limit:`, `where:` + `order:` without `limit:`, etc.). Each attribute is applied only when present, so an order-only scope is `WHERE`-free SQL.
1912
+
1913
+ ```yaml
1914
+ scopes:
1915
+ - name: top_open_deals
1916
+ where_not: { stage: [closed_won, closed_lost] }
1917
+ order: { value: desc }
1918
+ limit: 5
1919
+ ```
1920
+
1921
+ This generates: `scope :top_open_deals, -> { where.not(stage: [...]).order(value: :desc).limit(5) }`
1922
+
1923
+ ### NULL / Empty Value Scopes
1924
+
1925
+ YAML `null` maps to Ruby `nil`, which ActiveRecord translates to `WHERE column IS NULL`. This works in both `where` and `where_not` conditions.
1926
+
1927
+ ```yaml
1928
+ scopes:
1929
+ # Records where phone is NULL
1930
+ - name: without_phone
1931
+ where:
1932
+ phone: null
1933
+
1934
+ # Records where phone is NOT NULL
1935
+ - name: with_phone
1936
+ where_not:
1937
+ phone: null
1938
+
1939
+ # Records where phone is NULL or empty string
1940
+ - name: blank_phone
1941
+ where:
1942
+ phone: [null, ""]
1943
+ ```
1944
+
1945
+ Generated SQL:
1946
+
1947
+ | Scope | SQL |
1948
+ |-------|-----|
1949
+ | `without_phone` | `WHERE phone IS NULL` |
1950
+ | `with_phone` | `WHERE phone IS NOT NULL` |
1951
+ | `blank_phone` | `WHERE phone IS NULL OR phone = ''` |
1952
+
1953
+ ## Events
1954
+
1955
+ Events trigger [event handlers](../guides/event-handlers.md) in response to record changes.
1956
+
1957
+ ```yaml
1958
+ events:
1959
+ - name: after_create
1960
+ type: lifecycle
1961
+ - name: on_stage_change
1962
+ type: field_change
1963
+ field: stage
1964
+ condition:
1965
+ field: stage
1966
+ operator: not_in
1967
+ value: [lead]
1968
+ ```
1969
+
1970
+ ### Event Attributes
1971
+
1972
+ #### `name`
1973
+
1974
+ | | |
1975
+ |---|---|
1976
+ | **Required** | yes |
1977
+ | **Type** | string |
1978
+
1979
+ Event identifier. Matched against `HandlerBase.handles_event` in registered handlers. For lifecycle events, use one of the predefined names.
1980
+
1981
+ #### `type`
1982
+
1983
+ | | |
1984
+ |---|---|
1985
+ | **Required** | no |
1986
+ | **Default** | inferred from `name` |
1987
+ | **Allowed** | `lifecycle`, `field_change` |
1988
+
1989
+ - `lifecycle` — triggered by ActiveRecord callbacks
1990
+ - `field_change` — triggered when a specific field's value changes during an update
1991
+
1992
+ If omitted, the type is inferred: names matching `after_create`, `after_update`, `before_destroy`, or `after_destroy` are treated as lifecycle; all others as field_change.
1993
+
1994
+ #### `field`
1995
+
1996
+ | | |
1997
+ |---|---|
1998
+ | **Required** | yes (for `field_change` type) |
1999
+ | **Type** | string |
2000
+
2001
+ The field to monitor for changes. The event fires only when this field's value changes.
2002
+
2003
+ #### `condition`
2004
+
2005
+ | | |
2006
+ |---|---|
2007
+ | **Required** | no |
2008
+ | **Type** | hash (recommended) or string (deprecated) |
2009
+
2010
+ Optional condition that must be met for the event to fire. Uses the same `{ field:, operator:, value: }` syntax as [condition operators](condition-operators.md). When a `field_change` event fires, the condition is evaluated against the record's current state — the event is only dispatched if the condition returns true.
2011
+
2012
+ ```yaml
2013
+ # Hash condition (recommended)
2014
+ events:
2015
+ - name: on_stage_change
2016
+ type: field_change
2017
+ field: stage
2018
+ condition:
2019
+ field: stage
2020
+ operator: not_eq
2021
+ value: draft
2022
+ ```
2023
+
2024
+ ```ruby
2025
+ # DSL
2026
+ on_field_change :on_stage_change, field: :stage,
2027
+ condition: { field: :stage, operator: :not_in, value: %w[lead] }
2028
+ ```
2029
+
2030
+ > **Deprecated:** String conditions (evaluated via `instance_eval`) are deprecated and will be removed in a future version. Migrate to Hash conditions.
2031
+
2032
+ ### Lifecycle Events
2033
+
2034
+ | Name | Trigger |
2035
+ |------|---------|
2036
+ | `after_create` | After a new record is created |
2037
+ | `after_update` | After an existing record is updated |
2038
+ | `before_destroy` | Before a record is destroyed |
2039
+ | `after_destroy` | After a record is destroyed |
2040
+
2041
+ ## Display Templates
2042
+
2043
+ Display templates define rich HTML representations for records when displayed in contexts like `association_list`. Unlike `to_label` (which remains plain text), display templates support structured layouts with titles, subtitles, icons, and badges — or delegate to custom renderers/partials.
2044
+
2045
+ Templates live on the **model** (not the presenter), so the same record can be rendered consistently across different presenters. The presenter selects which template to use by name.
2046
+
2047
+ ### YAML Syntax
2048
+
2049
+ ```yaml
2050
+ display_templates:
2051
+ default:
2052
+ template: "{first_name} {last_name}"
2053
+ subtitle: "{position} at {company.name}"
2054
+ icon: user
2055
+ badge: "{status}"
2056
+ compact:
2057
+ template: "{last_name}, {first_name}"
2058
+ card:
2059
+ renderer: ContactCardRenderer
2060
+ mini:
2061
+ partial: "contacts/mini_label"
2062
+ ```
2063
+
2064
+ ### Three Forms
2065
+
2066
+ | Form | Detected by | Description |
2067
+ |------|-------------|-------------|
2068
+ | **Structured** | `template` key | Title, optional subtitle/icon/badge with `{field}` interpolation |
2069
+ | **Renderer** | `renderer` key | Delegates to a registered `Display::BaseRenderer` subclass |
2070
+ | **Partial** | `partial` key | Renders a Rails partial with `record` local |
2071
+
2072
+ ### Structured Template Keys
2073
+
2074
+ | Key | Type | Description |
2075
+ |-----|------|-------------|
2076
+ | `template` | string | **Required.** Main text with `{field}` placeholders |
2077
+ | `subtitle` | string | Secondary text below the title |
2078
+ | `icon` | string | Icon identifier (rendered as text; style via CSS) |
2079
+ | `badge` | string | Small label, supports `{field}` placeholders |
2080
+
2081
+ Field placeholders use the same dot-path syntax as presenter fields: `{field_name}` for direct fields, `{association.field}` for related records. The exact same `{field}` micro-syntax is also accepted by the [`computed`](#computed) field attribute when used in template form — both go through the same parser.
2082
+
2083
+ ### Renderer Form
2084
+
2085
+ | Key | Type | Description |
2086
+ |-----|------|-------------|
2087
+ | `renderer` | string | Class name of a registered `Display::BaseRenderer` |
2088
+ | `options` | hash | Passed to the renderer's `render` method |
2089
+
2090
+ ### Partial Form
2091
+
2092
+ | Key | Type | Description |
2093
+ |-----|------|-------------|
2094
+ | `partial` | string | Rails partial path (e.g., `"contacts/mini_label"`) |
2095
+
2096
+ ### DSL Syntax
2097
+
2098
+ ```ruby
2099
+ define_model :contact do
2100
+ display_template :default,
2101
+ template: "{first_name} {last_name}",
2102
+ subtitle: "{position} at {company.name}",
2103
+ icon: "user"
2104
+
2105
+ display_template :card, renderer: "ContactCardRenderer"
2106
+ display_template :mini, partial: "contacts/mini_label"
2107
+ end
2108
+ ```
2109
+
2110
+ ### Permission Filtering
2111
+
2112
+ Fields referenced in templates are resolved through `FieldValueResolver`, which respects the current user's `PermissionEvaluator`. If a field is not readable, it renders as blank rather than exposing unauthorized data.
2113
+
2114
+ ### Eager Loading
2115
+
2116
+ The `IncludesResolver` automatically detects dot-path fields in display templates (e.g., `{company.name}`) and generates nested eager loading (e.g., `{ contacts: :company }`) to prevent N+1 queries.
2117
+
2118
+ ## Virtual Columns
2119
+
2120
+ Virtual computed columns that are not stored in the database — they are calculated at query time and injected into SELECT statements. Virtual columns can be referenced in presenter table columns and show sections just like regular fields. They support sorting, conditional rendering (`visible_when`, `item_classes`), and type coercion.
2121
+
2122
+ Virtual column names must not collide with field names on the same model.
2123
+
2124
+ The `virtual_columns` key is the unified replacement for the older `aggregates` key. Both keys are accepted and merged — see [Backward Compatibility](#backward-compatibility) below.
2125
+
2126
+ ### YAML Syntax
2127
+
2128
+ ```yaml
2129
+ virtual_columns:
2130
+ # Declarative aggregate (same as before)
2131
+ issues_count:
2132
+ function: count
2133
+ association: issues
2134
+
2135
+ open_issues_count:
2136
+ function: count
2137
+ association: issues
2138
+ where: { status: open }
2139
+
2140
+ total_revenue:
2141
+ function: sum
2142
+ association: orders
2143
+ source_field: amount
2144
+ default: 0
2145
+
2146
+ # Expression — inline SQL expression (replaces "sql" key)
2147
+ is_overdue:
2148
+ expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
2149
+ type: boolean
2150
+
2151
+ # Expression with JOIN — pull data from another table
2152
+ company_name:
2153
+ expression: "companies.name"
2154
+ join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
2155
+ type: string
2156
+
2157
+ # Expression with JOIN + GROUP BY — aggregate across a joined table
2158
+ total_value:
2159
+ expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)"
2160
+ join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id"
2161
+ group: true
2162
+ type: decimal
2163
+
2164
+ # Auto-include — always loaded, even when not explicitly referenced
2165
+ priority_score:
2166
+ expression: "(%{table}.urgency * 10 + %{table}.impact * 5)"
2167
+ type: integer
2168
+ auto_include: true
2169
+
2170
+ # Service — Ruby service class
2171
+ health_score:
2172
+ service: project_health
2173
+ type: integer
2174
+ ```
2175
+
2176
+ ### DSL Syntax
2177
+
2178
+ ```ruby
2179
+ virtual_column :issues_count, function: :count, association: :issues
2180
+ virtual_column :open_issues_count, function: :count, association: :issues,
2181
+ where: { status: "open" }
2182
+ virtual_column :total_revenue, function: :sum, association: :orders,
2183
+ source_field: :amount, default: 0
2184
+ virtual_column :is_overdue,
2185
+ expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END",
2186
+ type: :boolean
2187
+ virtual_column :company_name,
2188
+ expression: "companies.name",
2189
+ join: "LEFT JOIN companies ON companies.id = %{table}.company_id",
2190
+ type: :string
2191
+ virtual_column :total_value,
2192
+ expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)",
2193
+ join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id",
2194
+ group: true,
2195
+ type: :decimal
2196
+
2197
+ # Legacy alias still works
2198
+ aggregate :contacts_count, function: :count, association: :contacts
2199
+ ```
2200
+
2201
+ ### Four Virtual Column Types
2202
+
2203
+ | Type | Detected by | Description |
2204
+ |------|-------------|-------------|
2205
+ | **Declarative** | `function` + `association` | SQL aggregate function over a has_many association (correlated subquery) |
2206
+ | **Expression** | `expression` key | Inline SQL expression injected into SELECT |
2207
+ | **Expression + JOIN** | `expression` + `join` | SQL expression that requires a JOIN clause |
2208
+ | **Service** | `service` key | Ruby service class from `app/lcp_services/virtual_columns/` (or `aggregates/`) |
2209
+
2210
+ ### Declarative Attributes
2211
+
2212
+ | Attribute | Type | Required | Description |
2213
+ |-----------|------|----------|-------------|
2214
+ | `function` | string | yes | SQL aggregate function: `count`, `sum`, `min`, `max`, `avg` |
2215
+ | `association` | string | yes | Name of a `has_many` association on the model |
2216
+ | `source_field` | string | for sum/min/max/avg | Field on the target model to aggregate. Optional for `count` (defaults to `*`) |
2217
+ | `where` | hash | no | Equality conditions on the target model (see [Where Conditions](#where-conditions)) |
2218
+ | `distinct` | boolean | no | Use `DISTINCT` in the aggregate function (default: `false`) |
2219
+ | `default` | any | no | Default value via `COALESCE`. `count` always defaults to `0` even without this |
2220
+ | `include_discarded` | boolean | no | Include soft-deleted records (default: `false`). Only relevant when the target model uses `soft_delete` |
2221
+
2222
+ ### Expression Attributes
2223
+
2224
+ | Attribute | Type | Required | Description |
2225
+ |-----------|------|----------|-------------|
2226
+ | `expression` | string | yes | SQL expression. Use `%{table}` for the parent model's quoted table name |
2227
+ | `type` | string | yes | Result type: `string`, `integer`, `float`, `decimal`, `boolean`, `date`, `datetime`, `json` |
2228
+ | `default` | any | no | Default value via `COALESCE` |
2229
+ | `join` | string | no | SQL JOIN clause to add. Use `%{table}` for the parent table name |
2230
+ | `group` | boolean | no | Whether this column requires `GROUP BY parent_table.id` (default: `false`) |
2231
+ | `auto_include` | boolean | no | Always include this column in queries, even when not referenced by the presenter (default: `false`) |
2232
+
2233
+ **Notes:**
2234
+ - `auto_include: true` and `group: true` cannot be combined on the same column.
2235
+ - `auto_include: true` only affects controller queries. Direct model queries (`Model.find`, `Model.where`) from custom actions, event handlers, or other Ruby code do not include virtual columns — use `VirtualColumns::Builder.apply` explicitly.
2236
+ - The `join` attribute is a single string, not an array. Concatenate multiple JOINs in one string if needed.
2237
+
2238
+ The legacy `sql` key is accepted as an alias for `expression`. You cannot specify both `sql` and `expression` on the same column.
2239
+
2240
+ ### Service Attributes
2241
+
2242
+ | Attribute | Type | Required | Description |
2243
+ |-----------|------|----------|-------------|
2244
+ | `service` | string | yes | Service key looked up in `app/lcp_services/virtual_columns/` (falls back to `app/lcp_services/aggregates/`) |
2245
+ | `type` | string | yes | Result type |
2246
+ | `options` | hash | no | Options hash passed to the service's `call` method |
2247
+
2248
+ The service class must implement `self.call(record, options:)`. Optionally implement `self.sql_expression(model_class, options:)` to return a SQL string — this enables sorting and avoids per-record evaluation.
2249
+
2250
+ **Note:** Service virtual columns without `sql_expression` are only resolved on show/edit pages (per-record via `call`). On index pages, they return `nil` — if referenced in `table_columns` or `item_classes`, the display will be empty and conditions will evaluate incorrectly.
2251
+
2252
+ ### Expression Details
2253
+
2254
+ #### `%{table}` Placeholder
2255
+
2256
+ The `%{table}` placeholder is replaced with the parent model's quoted table name at query time. Always use it to reference the parent table:
2257
+
2258
+ ```yaml
2259
+ # Good — portable across table names
2260
+ expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
2261
+
2262
+ # Bad — hardcoded table name
2263
+ expression: "CASE WHEN orders.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
2264
+ ```
2265
+
2266
+ #### JOIN Clause
2267
+
2268
+ The `join` attribute adds a SQL JOIN to the query. Multiple virtual columns can specify JOINs — duplicates are automatically deduplicated (compared case-insensitively after whitespace normalization):
2269
+
2270
+ ```yaml
2271
+ virtual_columns:
2272
+ company_name:
2273
+ expression: "companies.name"
2274
+ join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
2275
+ type: string
2276
+ company_country:
2277
+ expression: "companies.country"
2278
+ join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
2279
+ type: string
2280
+ ```
2281
+
2282
+ Both columns reference the same JOIN — it is applied only once.
2283
+
2284
+ #### GROUP BY
2285
+
2286
+ When `group: true` is set, the query adds `GROUP BY parent_table.id`. This is needed when the expression uses aggregate functions over joined rows:
2287
+
2288
+ ```yaml
2289
+ total_value:
2290
+ expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)"
2291
+ join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id"
2292
+ group: true
2293
+ type: decimal
2294
+ ```
2295
+
2296
+ **Caution:** Multiple JOINs combined with `group: true` can produce cartesian products (e.g., 3 line_items × 2 payments = 6 rows per order, inflating SUM results). When aggregating over multiple joined tables, prefer correlated subqueries in `expression:` instead of `join:` + `group:`:
2297
+
2298
+ ```yaml
2299
+ # Good — independent correlated subqueries, no cartesian product
2300
+ total_line_value:
2301
+ expression: "(SELECT COALESCE(SUM(li.quantity * li.unit_price), 0) FROM line_items li WHERE li.order_id = %{table}.id)"
2302
+ type: decimal
2303
+ total_payments:
2304
+ expression: "(SELECT COALESCE(SUM(p.amount), 0) FROM payments p WHERE p.order_id = %{table}.id)"
2305
+ type: decimal
2306
+
2307
+ # Avoid — two joins with group = cartesian risk
2308
+ ```
2309
+
2310
+ Reserve `join:` + `group: true` for cases where a single JOIN + GROUP is genuinely needed.
2311
+
2312
+ #### Auto-Include
2313
+
2314
+ Columns with `auto_include: true` are always loaded in every query context (index, show, edit) regardless of whether the presenter references them. This is useful for columns needed by conditional rendering or business logic:
2315
+
2316
+ ```yaml
2317
+ priority_score:
2318
+ expression: "(%{table}.urgency * 10 + %{table}.impact * 5)"
2319
+ type: integer
2320
+ auto_include: true
2321
+ ```
2322
+
2323
+ ### Where Conditions
2324
+
2325
+ The `where` hash applies equality conditions to **declarative** aggregate subqueries only. Expression columns embed their own WHERE conditions directly in the SQL string.
2326
+
2327
+ ```yaml
2328
+ where: { status: open } # WHERE status = 'open'
2329
+ where: { status: [open, in_progress] } # WHERE status IN ('open', 'in_progress')
2330
+ where: { deleted_at: null } # WHERE deleted_at IS NULL
2331
+ where: { status: active, priority: high } # WHERE status = 'active' AND priority = 'high'
2332
+ where: { assignee_id: :current_user } # WHERE assignee_id = <current_user.id>
2333
+ ```
2334
+
2335
+ The `:current_user` placeholder resolves to `current_user.id` at query time. When no user is signed in, it resolves to `nil` (producing `IS NULL`). This enables per-user aggregates like "my open issues count".
2336
+
2337
+ ### Type Inference
2338
+
2339
+ Declarative aggregates infer their result type automatically:
2340
+
2341
+ | Function | Inferred type |
2342
+ |----------|---------------|
2343
+ | `count` | `integer` (always) |
2344
+ | `sum`, `min`, `max` | Same as `source_field` type |
2345
+ | `avg` | `float` (or `decimal` if source is `decimal`) |
2346
+
2347
+ Expression and service virtual columns require an explicit `type` attribute.
2348
+
2349
+ ### Type Coercion
2350
+
2351
+ Virtual columns register an ActiveRecord `attribute` declaration at boot time, ensuring consistent Ruby types regardless of database adapter. For example, SQLite returns `0`/`1` for booleans while PostgreSQL returns `true`/`false` — the `attribute :is_overdue, :boolean` declaration normalizes both to Ruby `true`/`false`.
2352
+
2353
+ | Virtual column `type` | Ruby class |
2354
+ |----------------------|------------|
2355
+ | `string` | `String` |
2356
+ | `integer` | `Integer` |
2357
+ | `float` | `Float` |
2358
+ | `decimal` | `BigDecimal` |
2359
+ | `boolean` | `TrueClass`/`FalseClass` |
2360
+ | `date` | `Date` |
2361
+ | `datetime` | `Time` |
2362
+ | `text` | `String` |
2363
+
2364
+ **NULL safety for booleans:** SQL boolean expressions like `(due_date < CURRENT_DATE AND status != 'done')` return NULL (not FALSE) when any operand is NULL (e.g., `due_date` is NULL). After type coercion, `nil` ≠ `false` — a condition `{ operator: eq, value: false }` would not match `nil`. Use `default: false` on boolean virtual columns that use arithmetic/comparison operators. Exception: `EXISTS(...)` always returns TRUE/FALSE (never NULL), so `default:` is not needed for EXISTS expressions.
2365
+
2366
+ ### Soft Delete Awareness
2367
+
2368
+ When the target model uses `soft_delete`, **declarative** aggregates automatically exclude soft-deleted records (`WHERE discarded_at IS NULL`). Set `include_discarded: true` to include them.
2369
+
2370
+ For **expression** columns with correlated subqueries or JOINs referencing a soft-deletable model, you must filter discarded records manually in the SQL (e.g., `AND discarded_at IS NULL`). The platform does not inject soft-delete conditions into raw SQL expressions.
2371
+
2372
+ ### Presenter Usage
2373
+
2374
+ Virtual columns are referenced in presenters as regular fields:
2375
+
2376
+ ```yaml
2377
+ # Index — sortable virtual column
2378
+ table_columns:
2379
+ - { field: issues_count, sortable: true }
2380
+ - { field: total_revenue, renderer: currency, sortable: true }
2381
+ - { field: is_overdue }
2382
+ - { field: company_name }
2383
+
2384
+ # Show — in a section
2385
+ layout:
2386
+ - section: "Statistics"
2387
+ fields:
2388
+ - { field: issues_count }
2389
+ - { field: total_revenue, renderer: currency }
2390
+ - { field: company_name }
2391
+ ```
2392
+
2393
+ Virtual columns are visible to all roles regardless of field permissions — they are computed values, not stored data.
2394
+
2395
+ #### Explicit Virtual Column Lists
2396
+
2397
+ Presenters can explicitly list virtual columns to include, beyond what is auto-detected from field references:
2398
+
2399
+ ```yaml
2400
+ # In index config
2401
+ index:
2402
+ table_columns:
2403
+ - { field: title }
2404
+ virtual_columns: [total_value, is_overdue]
2405
+
2406
+ # In show config
2407
+ show:
2408
+ virtual_columns: [total_value]
2409
+ layout:
2410
+ - section: "Details"
2411
+ fields:
2412
+ - { field: title }
2413
+ ```
2414
+
2415
+ ### Auto-Detection (Collector)
2416
+
2417
+ The system automatically detects which virtual columns are needed for each page context by scanning:
2418
+
2419
+ | Source | Context |
2420
+ |--------|---------|
2421
+ | `table_columns` field names | `:index` |
2422
+ | `tile.fields` field names | `:index` |
2423
+ | `item_classes[].when` conditions (recursive through `all`/`any`/`not`) | `:index` |
2424
+ | Permission `record_rules[].when` conditions (all roles, recursive) | `:index` |
2425
+ | Action `visible_when` / `disable_when` conditions (single, collection, batch) | `:index`, `:show`, `:edit` |
2426
+ | Show `layout` field names | `:show` |
2427
+ | Form field/section `visible_when` / `disable_when` conditions | `:edit` |
2428
+ | Explicit `virtual_columns` lists | per context |
2429
+ | `auto_include: true` columns | all contexts |
2430
+ | Sort params at runtime (`?sort=field_name`) | `:index` (dynamic) |
2431
+
2432
+ This means you don't need to manually list virtual columns in most cases — referencing a virtual column in a table column or condition is sufficient.
2433
+
2434
+ **Note:** `record_rules` auto-detection scans all roles (not just the current user's role) to collect virtual column names. Including an unused virtual column in SELECT is harmless, while missing one would cause a runtime error.
2435
+
2436
+ ### Backward Compatibility
2437
+
2438
+ The `aggregates` YAML key continues to work and is merged with `virtual_columns`:
2439
+
2440
+ ```yaml
2441
+ # Both keys accepted — merged into a single set
2442
+ aggregates:
2443
+ issues_count:
2444
+ function: count
2445
+ association: issues
2446
+
2447
+ virtual_columns:
2448
+ is_overdue:
2449
+ expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
2450
+ type: boolean
2451
+ ```
2452
+
2453
+ A name collision between the two keys raises a `MetadataError`.
2454
+
2455
+ The Ruby API also preserves backward compatibility:
2456
+
2457
+ | Legacy method | Aliased to |
2458
+ |---------------|------------|
2459
+ | `model_def.aggregates` | `model_def.virtual_columns` |
2460
+ | `model_def.aggregate(name)` | `model_def.virtual_column(name)` |
2461
+ | `model_def.aggregate_names` | `model_def.virtual_column_names` |
2462
+ | DSL `aggregate(...)` | DSL `virtual_column(...)` |
2463
+
2464
+ The legacy `sql` key in YAML is accepted as an alias for `expression`.
2465
+
2466
+ ### Limitations
2467
+
2468
+ - **Not filterable via Ransack** — Virtual columns are SQL aliases, not real database columns. They are not added to `ransackable_attributes` and cannot be used in the advanced filter builder or saved filters. To filter by a virtual column value, use a custom scope.
2469
+ - **Not in summary bar / column summaries** — Both `summary_bar` and column-level `summary` operate on real database columns via `scope.sum(field)` etc. Virtual column names are silently skipped.
2470
+ - **Permission scopes cannot reference virtual columns** — `ScopeBuilder`'s `where` and `field_match` types use `.where()` which requires real columns. Use `type: custom` with a named scope that embeds the SQL expression.
2471
+ - **Read-only in forms** — Virtual columns are computed values and never appear in `permitted_params`. If referenced in a form layout, they render as read-only display fields.
2472
+ - **DB portability is the configurator's responsibility** — The platform does not abstract SQL differences between PostgreSQL and SQLite in `expression:` and `join:` strings. For DB-portable logic, use service virtual columns with Arel.
2473
+ - **Security** — `expression:` and `join:` values are defined in YAML/DSL by the platform configurator and are not user input. The `%{table}` placeholder is resolved to a properly quoted table name. No user-supplied values are interpolated into these strings.
2474
+
2475
+ ### Complete Example
2476
+
2477
+ ```yaml
2478
+ model:
2479
+ name: order
2480
+ fields:
2481
+ - { name: title, type: string }
2482
+ - { name: status, type: string }
2483
+ - { name: due_date, type: date }
2484
+ associations:
2485
+ - { type: has_many, name: line_items, target_model: line_item, foreign_key: order_id }
2486
+ - { type: belongs_to, name: company, target_model: company, foreign_key: company_id, required: false }
2487
+ virtual_columns:
2488
+ items_count:
2489
+ function: count
2490
+ association: line_items
2491
+ total_value:
2492
+ expression: "COALESCE(SUM(line_items.quantity * line_items.unit_price), 0)"
2493
+ join: "LEFT JOIN line_items ON line_items.order_id = %{table}.id"
2494
+ group: true
2495
+ type: decimal
2496
+ is_overdue:
2497
+ expression: "CASE WHEN %{table}.due_date < CURRENT_DATE THEN 1 ELSE 0 END"
2498
+ type: boolean
2499
+ company_name:
2500
+ expression: "companies.name"
2501
+ join: "LEFT JOIN companies ON companies.id = %{table}.company_id"
2502
+ type: string
2503
+ priority_score:
2504
+ expression: "(%{table}.urgency * 10 + %{table}.impact * 5)"
2505
+ type: integer
2506
+ auto_include: true
2507
+ ```
2508
+
2509
+ ## Options
2510
+
2511
+ ```yaml
2512
+ options:
2513
+ timestamps: true
2514
+ label_method: title
2515
+ sti: true # enable Single Table Inheritance on this model
2516
+ soft_delete: true # or { column: deleted_at }
2517
+ auditing: true # or { only: [title, status], ... }
2518
+ userstamps: true # or { created_by: author_id, updated_by: editor_id, store_name: true }
2519
+ tree: true # or { parent_field: parent_category_id, ... }
2520
+ ```
2521
+
2522
+ ### `timestamps`
2523
+
2524
+ | | |
2525
+ |---|---|
2526
+ | **Default** | `true` |
2527
+ | **Type** | boolean |
2528
+
2529
+ Adds `created_at` and `updated_at` columns to the table.
2530
+
2531
+ ### `label_method`
2532
+
2533
+ | | |
2534
+ |---|---|
2535
+ | **Default** | `"to_s"` |
2536
+ | **Type** | string |
2537
+
2538
+ Method called on records to generate display text (e.g., in association select dropdowns). Should return a human-readable string.
2539
+
2540
+ ### `sti`
2541
+
2542
+ | | |
2543
+ |---|---|
2544
+ | **Default** | `false` |
2545
+ | **Type** | boolean |
2546
+
2547
+ Enables Single Table Inheritance (STI) on this model. See [Single Table Inheritance](#single-table-inheritance-sti) for details.
2548
+
2549
+ ### `custom_fields`
2550
+
2551
+ | | |
2552
+ |---|---|
2553
+ | **Default** | `false` |
2554
+ | **Type** | boolean |
2555
+
2556
+ Enables user-defined custom fields on this model. When `true`, the engine:
2557
+ - Adds a `custom_data` column (JSONB on PostgreSQL, JSON on SQLite) to the model's table
2558
+ - Creates a GIN index on the column (PostgreSQL only)
2559
+ - Installs dynamic accessor methods for each custom field definition
2560
+ - Validates custom field values based on their definition constraints
2561
+ - Auto-generates a management presenter at `/custom-fields-<model_name>`
2562
+
2563
+ Custom field definitions are stored in the built-in `custom_field_definition` model. Access is controlled through the `custom_data` virtual field in permissions.
2564
+
2565
+ ```yaml
2566
+ options:
2567
+ custom_fields: true
2568
+ ```
2569
+
2570
+ See [Custom Fields Reference](custom-fields.md) for the complete attribute reference and [Custom Fields Guide](../guides/custom-fields.md) for a step-by-step tutorial.
2571
+
2572
+ ### `soft_delete`
2573
+
2574
+ | | |
2575
+ |---|---|
2576
+ | **Default** | `false` (disabled) |
2577
+ | **Type** | `true` or Hash |
2578
+
2579
+ Enables soft delete (logical deletion) for this model. Instead of permanently deleting records, the engine sets a timestamp column to mark them as discarded.
2580
+
2581
+ **Simple form** — uses default column `discarded_at`:
2582
+
2583
+ ```yaml
2584
+ options:
2585
+ soft_delete: true
2586
+ ```
2587
+
2588
+ **Hash form** — custom column name:
2589
+
2590
+ ```yaml
2591
+ options:
2592
+ soft_delete:
2593
+ column: deleted_at
2594
+ ```
2595
+
2596
+ | Key | Type | Default | Description |
2597
+ |-----|------|---------|-------------|
2598
+ | `column` | string | `"discarded_at"` | Name of the datetime column that stores the discard timestamp |
2599
+
2600
+ When `soft_delete` is enabled, the engine automatically:
2601
+
2602
+ - Creates the timestamp column (`discarded_at` by default) plus tracking columns (`discarded_by_type`, `discarded_by_id`)
2603
+ - Adds scopes: `kept` (non-discarded), `discarded` (discarded only), `with_discarded` (all records)
2604
+ - Adds instance methods: `discard!`, `undiscard!`, `discarded?`, `kept?`, `cascade_discarded?`
2605
+ - Changes the controller `destroy` action to soft-delete (sets `discarded_at`) instead of hard-delete
2606
+ - Filters discarded records from the default index view (applies `kept` scope)
2607
+ - Supports `dependent: :discard` on `has_many` associations for automatic cascade discard/undiscard
2608
+
2609
+ The `discard!(by:)` method accepts an optional `by:` parameter to track which record triggered the discard (used for cascade tracking). The `undiscard!` method only restores cascade-discarded children — manually discarded records are left as-is.
2610
+
2611
+ #### Cascade behavior with `has_many dependent:`
2612
+
2613
+ `discard!` is **not** the same call path as `destroy!`. ActiveRecord's `dependent:` rules only fire for actual destroys, so the parent's children are handled like this:
2614
+
2615
+ | `dependent:` on `has_many` | Effect when parent is `discard!`-ed |
2616
+ |---|---|
2617
+ | `:discard` | Children are also soft-deleted (cascade tracked — `undiscard!` will restore them). **Both parent and child models must have `soft_delete` enabled.** |
2618
+ | `:destroy`, `:delete`, `:delete_all` | **Not cascaded.** Children remain untouched and become orphans (visible via the parent's `with_discarded` scope only). |
2619
+ | `:nullify` | Not cascaded — children keep their FK. |
2620
+ | `:restrict_with_exception`, `:restrict_with_error` | Not cascaded — children block the **destroy** path, but `discard!` bypasses the check. |
2621
+ | (omitted) | Not cascaded. |
2622
+
2623
+ Mix `:destroy` and `soft_delete` only when you actually want to drop children permanently while keeping the parent recoverable; otherwise prefer `:discard` everywhere in a soft-delete graph for a coherent restore story.
2624
+
2625
+ See [Soft Delete Guide](../guides/soft-delete.md) for setup and usage examples.
2626
+
2627
+ ### `auditing`
2628
+
2629
+ | | |
2630
+ |---|---|
2631
+ | **Default** | `false` (disabled) |
2632
+ | **Type** | `true`, `false`, or Hash |
2633
+
2634
+ Enables change auditing for this model. When enabled, all field changes are tracked and stored via the configured `audit_writer`. Set to `false` to explicitly disable (same as omitting the key).
2635
+
2636
+ **Simple form** — tracks all fields:
2637
+
2638
+ ```yaml
2639
+ options:
2640
+ auditing: true
2641
+ ```
2642
+
2643
+ **Hash form** — fine-grained control:
2644
+
2645
+ ```yaml
2646
+ options:
2647
+ auditing:
2648
+ only:
2649
+ - title
2650
+ - status
2651
+ track_associations: true
2652
+ track_attachments: true
2653
+ expand_custom_fields: true
2654
+ expand_json_fields:
2655
+ - addresses
2656
+ ```
2657
+
2658
+ | Key | Type | Default | Description |
2659
+ |-----|------|---------|-------------|
2660
+ | `only` | array of strings | all fields | Track only these fields. Mutually exclusive with `ignore`. |
2661
+ | `ignore` | array of strings | none | Track all fields except these. Mutually exclusive with `only`. |
2662
+ | `track_associations` | boolean | `true` | Include nested_attributes child changes in parent audit entry |
2663
+ | `track_attachments` | boolean | `false` | Include attachment changes in audit trail (reserved, not yet implemented) |
2664
+ | `expand_custom_fields` | boolean | `true` | Expand `custom_data` JSON into individual `cf:` prefixed field changes |
2665
+ | `expand_json_fields` | array of strings | `[]` | JSON columns to expand into dot-path key changes |
2666
+
2667
+ > **Note:** `only` and `ignore` are mutually exclusive — specifying both causes a validation error.
2668
+
2669
+ See [Auditing Reference](auditing.md) for the audit log model contract, changes data format, and configuration options. See [Auditing Guide](../guides/auditing.md) for setup and usage examples.
2670
+
2671
+ ### `userstamps`
2672
+
2673
+ | | |
2674
+ |---|---|
2675
+ | **Default** | `false` (disabled) |
2676
+ | **Type** | `true` or Hash |
2677
+
2678
+ Enables automatic user tracking — stores the ID of the user who created and last updated each record via a `before_save` callback that reads from `LcpRuby::Current.user`.
2679
+
2680
+ **Simple form** — uses default columns `created_by_id` and `updated_by_id`:
2681
+
2682
+ ```yaml
2683
+ options:
2684
+ userstamps: true
2685
+ ```
2686
+
2687
+ **Hash form** — custom column names and name snapshots:
2688
+
2689
+ ```yaml
2690
+ options:
2691
+ userstamps:
2692
+ created_by: author_id
2693
+ updated_by: editor_id
2694
+ store_name: true
2695
+ ```
2696
+
2697
+ | Key | Type | Default | Description |
2698
+ |-----|------|---------|-------------|
2699
+ | `created_by` | string | `"created_by_id"` | Column name for the creating user's FK |
2700
+ | `updated_by` | string | `"updated_by_id"` | Column name for the last updating user's FK |
2701
+ | `store_name` | boolean | `false` | Add denormalized `_name` snapshot columns (e.g., `created_by_name`, `updated_by_name`) |
2702
+
2703
+ The applicator automatically:
2704
+ - Adds `bigint` columns (nullable, indexed) for creator and updater FK
2705
+ - Adds `string` columns for name snapshots when `store_name: true`
2706
+ - Adds `belongs_to` associations pointing to `LcpRuby.configuration.user_class`
2707
+ - Sets `created_by_id` only on `new_record?`, `updated_by_id` on every save
2708
+ - Writes `nil` when `LcpRuby::Current.user` is not set (seeds, jobs, console)
2709
+ - `update_columns` bypasses the callback by design (same as Rails timestamps)
2710
+
2711
+ ### `tree`
2712
+
2713
+ | | |
2714
+ |---|---|
2715
+ | **Default** | `false` (disabled) |
2716
+ | **Type** | `true` or Hash |
2717
+
2718
+ Enables tree/hierarchy structure for this model. The model becomes self-referencing with parent-child relationships, enabling nested structures like categories, organizational units, or threaded comments.
2719
+
2720
+ **Simple form** — uses defaults:
2721
+
2722
+ ```yaml
2723
+ options:
2724
+ tree: true
2725
+ ```
2726
+
2727
+ **Hash form** — custom configuration:
2728
+
2729
+ ```yaml
2730
+ options:
2731
+ tree:
2732
+ parent_field: parent_category_id
2733
+ parent_name: parent_category
2734
+ children_name: subcategories
2735
+ dependent: nullify
2736
+ max_depth: 5
2737
+ ordered: true
2738
+ position_field: position
2739
+ ```
2740
+
2741
+ | Key | Type | Default | Description |
2742
+ |-----|------|---------|-------------|
2743
+ | `parent_field` | string | `"parent_id"` | Foreign key column for the parent record |
2744
+ | `parent_name` | string | `"parent"` | Name of the `belongs_to` parent association |
2745
+ | `children_name` | string | `"children"` | Name of the `has_many` children association |
2746
+ | `dependent` | string | `"destroy"` | What happens to children when parent is deleted. Values: `destroy`, `nullify`, `restrict_with_exception`, `restrict_with_error`, `discard` (requires soft_delete) |
2747
+ | `max_depth` | integer | `10` | Maximum allowed tree depth. Enforced by cycle detection validation |
2748
+ | `ordered` | boolean | `false` | Enable position-based ordering of siblings. Automatically configures `positioning` scoped to parent |
2749
+ | `position_field` | string | `"position"` | Column name for sibling ordering (only used when `ordered: true`) |
2750
+
2751
+ The `TreeApplicator` automatically:
2752
+ - Creates `belongs_to` / `has_many` self-referential associations
2753
+ - Adds `roots` and `leaves` scopes
2754
+ - Adds traversal instance methods: `root?`, `leaf?`, `ancestors`, `descendants`, `subtree`, `subtree_ids`, `siblings`, `depth`, `path`, `root`
2755
+ - Adds cycle detection validation (prevents self-reference, circular chains, and max_depth violations)
2756
+ - Adds a database index on the parent field
2757
+ - When `ordered: true`, configures `positioning` scoped to the parent field (unless the model already has explicit positioning)
2758
+
2759
+ See [Tree Structures Reference](tree-structures.md) for full details on scopes, methods, and presenter integration.
2760
+
2761
+ ## Complete Example
2762
+
2763
+ ### YAML (TODO App)
2764
+
2765
+ ```yaml
2766
+ model:
2767
+ name: todo_item
2768
+ label: "Todo Item"
2769
+ label_plural: "Todo Items"
2770
+
2771
+ fields:
2772
+ - name: title
2773
+ type: string
2774
+ label: "Title"
2775
+ transforms: [strip]
2776
+ column_options:
2777
+ limit: 255
2778
+ "null": false
2779
+ validations:
2780
+ - type: presence
2781
+
2782
+ - name: completed
2783
+ type: boolean
2784
+ label: "Completed"
2785
+ default: false
2786
+
2787
+ - name: start_date
2788
+ type: date
2789
+ label: "Start Date"
2790
+ default: current_date
2791
+
2792
+ - name: due_date
2793
+ type: date
2794
+ label: "Due Date"
2795
+ default:
2796
+ service: one_week_from_now
2797
+ validations:
2798
+ - type: comparison
2799
+ operator: gte
2800
+ field_ref: start_date
2801
+ message: "must be on or after start date"
2802
+ - type: presence
2803
+ when:
2804
+ field: completed
2805
+ operator: eq
2806
+ value: false
2807
+
2808
+ associations:
2809
+ - type: belongs_to
2810
+ name: todo_list
2811
+ target_model: todo_list
2812
+ foreign_key: todo_list_id
2813
+ required: true
2814
+
2815
+ options:
2816
+ timestamps: true
2817
+ label_method: title
2818
+ ```
2819
+
2820
+ ### Ruby DSL (CRM App)
2821
+
2822
+ ```ruby
2823
+ define_model :deal do
2824
+ label "Deal"
2825
+ label_plural "Deals"
2826
+
2827
+ field :title, :string, label: "Title", limit: 255, null: false do
2828
+ validates :presence
2829
+ end
2830
+
2831
+ field :stage, :enum, label: "Stage", default: "lead",
2832
+ values: {
2833
+ lead: "Lead", qualified: "Qualified", proposal: "Proposal",
2834
+ negotiation: "Negotiation", closed_won: "Closed Won",
2835
+ closed_lost: "Closed Lost"
2836
+ }
2837
+
2838
+ field :value, :decimal, label: "Value", precision: 12, scale: 2 do
2839
+ validates :numericality, greater_than_or_equal_to: 0, allow_nil: true
2840
+ validates :presence, when: { field: :stage, operator: :not_in, value: %w[lead] }
2841
+ end
2842
+
2843
+ field :weighted_value, :decimal, label: "Weighted Value", precision: 12, scale: 2,
2844
+ computed: { service: "weighted_deal_value" }
2845
+
2846
+ field :expected_close_date, :date, label: "Expected Close",
2847
+ default: { service: "thirty_days_out" } do
2848
+ validates :comparison, operator: :gte, field_ref: :created_at,
2849
+ message: "cannot be before deal creation"
2850
+ end
2851
+
2852
+ belongs_to :company, model: :company, required: true
2853
+ belongs_to :contact, model: :contact, required: false
2854
+
2855
+ validates :contact_id, :presence,
2856
+ when: { field: :stage, operator: :in, value: %w[negotiation closed_won closed_lost] }
2857
+
2858
+ validates_model :service, service: "deal_credit_limit"
2859
+
2860
+ scope :open_deals, where_not: { stage: ["closed_won", "closed_lost"] }
2861
+
2862
+ on_field_change :on_stage_change, field: :stage,
2863
+ condition: { field: :stage, operator: :not_in, value: %w[lead] }
2864
+
2865
+ timestamps true
2866
+ label_method :title
2867
+ end
2868
+ ```
2869
+
2870
+ ## Single Table Inheritance (STI)
2871
+
2872
+ STI allows multiple models to share a single database table, with a `type` column distinguishing the AR class. Each child model gets its own presenter, permissions, workflow, and pages.
2873
+
2874
+ ### Enabling STI
2875
+
2876
+ Set `options.sti: true` on the parent model:
2877
+
2878
+ ```yaml
2879
+ # models/document.yml
2880
+ model:
2881
+ name: document
2882
+ options:
2883
+ sti: true
2884
+ fields:
2885
+ - name: title
2886
+ type: string
2887
+ - name: status
2888
+ type: enum
2889
+ enum_values: [draft, published]
2890
+ ```
2891
+
2892
+ Define child models with `inherits:` pointing to the STI parent:
2893
+
2894
+ ```yaml
2895
+ # models/invoice.yml
2896
+ model:
2897
+ name: invoice
2898
+ inherits: document
2899
+ fields:
2900
+ - name: amount
2901
+ type: decimal
2902
+ - name: due_date
2903
+ type: date
2904
+ ```
2905
+
2906
+ ```yaml
2907
+ # models/credit_note.yml
2908
+ model:
2909
+ name: credit_note
2910
+ inherits: document
2911
+ fields:
2912
+ - name: reason
2913
+ type: text
2914
+ ```
2915
+
2916
+ ### How it works
2917
+
2918
+ - A single `documents` table is created with a `type` column, plus columns from all children (`amount`, `due_date`, `reason`).
2919
+ - `LcpRuby::Dynamic::Invoice` inherits from `LcpRuby::Dynamic::Document` at the Ruby level.
2920
+ - `Invoice.all` returns only invoices; `Document.all` returns all types.
2921
+ - Parent validations, enums, callbacks, and scopes are inherited via AR class hierarchy (not re-applied).
2922
+ - Each child can have its own presenter (with its own slug, columns, form) and its own permissions.
2923
+
2924
+ ### Restrictions
2925
+
2926
+ - STI children must not define `table_name` (they inherit the parent's table).
2927
+ - STI children must not set `options.sti: true` (only the root parent can be STI).
2928
+ - Abstract models cannot be STI parents (use `abstract: true` or `options.sti: true`, not both).
2929
+
2930
+ ### Permission fallback
2931
+
2932
+ When no permissions are defined for an STI child, the platform falls back to the parent model's permissions, then to `_default`. This applies to both YAML and DB permission sources.
2933
+
2934
+ ### Workflow fallback
2935
+
2936
+ When no workflow is defined for an STI child, the platform falls back to the parent model's workflow.
2937
+
2938
+ ### Chaining with abstract inheritance
2939
+
2940
+ An abstract → STI chain is supported: define common fields in an abstract base, then an STI parent that inherits from it, then STI children:
2941
+
2942
+ ```yaml
2943
+ # models/_auditable.yml
2944
+ model:
2945
+ name: _auditable
2946
+ abstract: true
2947
+ fields:
2948
+ - name: created_by
2949
+ type: string
2950
+
2951
+ # models/document.yml
2952
+ model:
2953
+ name: document
2954
+ inherits: _auditable
2955
+ options:
2956
+ sti: true
2957
+ fields:
2958
+ - name: title
2959
+ type: string
2960
+
2961
+ # models/invoice.yml
2962
+ model:
2963
+ name: invoice
2964
+ inherits: document
2965
+ fields:
2966
+ - name: amount
2967
+ type: decimal
2968
+ ```
2969
+
2970
+ In this chain, `invoice` gets `created_by` (from `_auditable`), `title` (from `document`), and `amount` (its own).
2971
+
2972
+ Source: `lib/lcp_ruby/metadata/model_definition.rb`, `lib/lcp_ruby/metadata/field_definition.rb`, `lib/lcp_ruby/metadata/validation_definition.rb`, `lib/lcp_ruby/metadata/association_definition.rb`, `lib/lcp_ruby/metadata/event_definition.rb`