ecoportal-api-graphql 1.3.5 → 1.3.9

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 (499) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/bridge/CLAUDE.md +338 -0
  3. data/.ai-assistance/bridge/archive/.gitkeep +0 -0
  4. data/.ai-assistance/bridge/archive/oscar-a1b2c3d-gitlab-mcp-doc-update.inbox.md +29 -0
  5. data/.ai-assistance/bridge/archive/oscar-a1b2c3d-gitlab-mcp-doc-update.outbox.md +18 -0
  6. data/.ai-assistance/bridge/archive/oscar-c912c25-gemini-design-review.inbox.md +42 -0
  7. data/.ai-assistance/bridge/archive/oscar-c912c25-gemini-design-review.outbox.md +115 -0
  8. data/.ai-assistance/bridge/context/gemini-review-prompt.txt +48 -0
  9. data/.ai-assistance/bridge/context/gemini-review-response.md +104 -0
  10. data/.ai-assistance/bridge/context/project.md +42 -0
  11. data/.ai-assistance/bridge/inbox/.gitkeep +0 -0
  12. data/.ai-assistance/bridge/outbox/.gitkeep +0 -0
  13. data/.ai-assistance/bridge/outbox/request-for-standards-discovery.md +48 -0
  14. data/.ai-assistance/bridge/queue/.gitkeep +1 -0
  15. data/.ai-assistance/capabilities/CLAUDE.md +27 -0
  16. data/.ai-assistance/capabilities/assumptions-log.md +80 -0
  17. data/.ai-assistance/capabilities/code.md +47 -0
  18. data/.ai-assistance/capabilities/connectors.md +37 -0
  19. data/.ai-assistance/capabilities/cowork.md +55 -0
  20. data/.ai-assistance/code/OVERVIEW.md +155 -0
  21. data/.ai-assistance/code/data_fields.md +242 -0
  22. data/.ai-assistance/code/dependencies.md +151 -0
  23. data/.ai-assistance/code/diff_as_input.md +234 -0
  24. data/.ai-assistance/code/diff_service_deep_dive.md +192 -0
  25. data/.ai-assistance/code/ecoPortal_architecture/00_overview_and_index.md +55 -0
  26. data/.ai-assistance/code/ecoPortal_architecture/01_terminology_dictionary.md +181 -0
  27. data/.ai-assistance/code/ecoPortal_architecture/02_data_model.md +192 -0
  28. data/.ai-assistance/code/ecoPortal_architecture/03_api_layers.md +147 -0
  29. data/.ai-assistance/code/ecoPortal_architecture/04_graphql_queries_mutations.md +277 -0
  30. data/.ai-assistance/code/ecoPortal_architecture/05_page_workflows.md +200 -0
  31. data/.ai-assistance/code/ecoPortal_architecture/06_search_and_filters.md +228 -0
  32. data/.ai-assistance/code/ecoPortal_architecture/07_data_fields.md +197 -0
  33. data/.ai-assistance/code/ecoPortal_architecture/08_stages_sections.md +243 -0
  34. data/.ai-assistance/code/ecoPortal_architecture/09_people_contractors_locations.md +196 -0
  35. data/.ai-assistance/code/ecoPortal_architecture/10_forces_workflow_builder.md +132 -0
  36. data/.ai-assistance/code/ecoPortal_architecture/11_integration_gems.md +187 -0
  37. data/.ai-assistance/code/ecoPortal_architecture/12_ai_documentation_sources_gaps.md +236 -0
  38. data/.ai-assistance/code/ecoPortal_architecture/13_ai_infrastructure.md +183 -0
  39. data/.ai-assistance/code/ecoportal_schema_reference.md +240 -0
  40. data/.ai-assistance/code/graphql_domain_knowledge.md +230 -0
  41. data/.ai-assistance/code/refactoring/datafield-readwrite-shape-asymmetry.md +71 -0
  42. data/.ai-assistance/code/refactoring/opportunities.md +251 -0
  43. data/.ai-assistance/code/schema_analysis.md +321 -0
  44. data/.ai-assistance/code/search_filters.md +868 -0
  45. data/.ai-assistance/code/spec_coverage.md +73 -0
  46. data/.ai-assistance/code/workflow-command-guide.md +438 -0
  47. data/.ai-assistance/code/workflow-space.md +353 -0
  48. data/.ai-assistance/conventions/CLAUDE.md +30 -0
  49. data/.ai-assistance/conventions/code-working-tree-protocol.md +199 -0
  50. data/.ai-assistance/conventions/gitignore-rules.md +42 -0
  51. data/.ai-assistance/conventions/permission-guidance.md +120 -0
  52. data/.ai-assistance/integrations/README.md +70 -0
  53. data/.ai-assistance/integrations/gitkraken-mcp.md +107 -0
  54. data/.ai-assistance/integrations/gitlab-mcp.md +123 -0
  55. data/.ai-assistance/integrations/local-git.md +60 -0
  56. data/.ai-assistance/local_paths.example.md +17 -0
  57. data/.ai-assistance/projects/TODO.md +97 -0
  58. data/.ai-assistance/projects/api-v2-to-graphql-migration/DECISIONS.md +168 -0
  59. data/.ai-assistance/projects/api-v2-to-graphql-migration/INTENT.md +60 -0
  60. data/.ai-assistance/projects/api-v2-to-graphql-migration/TODO.md +267 -0
  61. data/.ai-assistance/projects/api-v2-to-graphql-migration/UPSTREAM.md +53 -0
  62. data/.ai-assistance/projects/api-v2-to-graphql-migration/notes/csv-template-pipeline-design.md +102 -0
  63. data/.ai-assistance/projects/api-v2-to-graphql-migration/notes/cutover-usecase-gap-audit.md +139 -0
  64. data/.ai-assistance/projects/dynamic-model-generation/INTENT.md +93 -0
  65. data/.ai-assistance/projects/eco-helpers-compat/INTENT.md +244 -0
  66. data/.ai-assistance/projects/eco-helpers-compat/MIGRATION_GUIDE.md +266 -0
  67. data/.ai-assistance/projects/eco-helpers-compat/TODO.md +86 -0
  68. data/.ai-assistance/projects/ecoportal-api-v2-doublemodel-review/INTENT.md +101 -0
  69. data/.ai-assistance/projects/graphql-agent/GAP_ANALYSIS.md +177 -0
  70. data/.ai-assistance/projects/ooze-graphql-native-migration/DECISIONS.md +161 -0
  71. data/.ai-assistance/projects/ooze-graphql-native-migration/INTENT.md +125 -0
  72. data/.ai-assistance/projects/ooze-graphql-native-migration/RISKS.md +126 -0
  73. data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +256 -0
  74. data/.ai-assistance/projects/ooze-graphql-native-migration/analysis/2026-06-30-cutover-workflow-deep-review.md +122 -0
  75. data/.ai-assistance/projects/ooze-graphql-native-migration/analysis/2026-07-01-forces-via-workflow-commands-miss-rca.md +148 -0
  76. data/.ai-assistance/projects/page-model/DECISIONS.md +245 -0
  77. data/.ai-assistance/projects/page-model/TODO.md +190 -0
  78. data/.ai-assistance/projects/search-filter-builder/INTENT.md +107 -0
  79. data/.ai-assistance/projects/search-filter-builder/TODO.md +131 -0
  80. data/.ai-assistance/projects/template-maintenance/DESIGN.md +134 -0
  81. data/.ai-assistance/projects/workflow-space/TODO.md +213 -0
  82. data/.ai-assistance/reinstall-claude-desktop-windows.md +136 -0
  83. data/.ai-assistance/scripts/CLAUDE.md +150 -0
  84. data/.ai-assistance/scripts/bridge-init.sh +86 -0
  85. data/.ai-assistance/scripts/bridge-status.sh +44 -0
  86. data/.ai-assistance/scripts/capabilities-check.ts +104 -0
  87. data/.ai-assistance/scripts/check-outbox.sh +43 -0
  88. data/.ai-assistance/scripts/dep_graph.rb +91 -0
  89. data/.ai-assistance/scripts/lock-acquire.sh +103 -0
  90. data/.ai-assistance/scripts/lock-multi.sh +124 -0
  91. data/.ai-assistance/scripts/lock-queue.sh +94 -0
  92. data/.ai-assistance/scripts/setup-mcps.test.ts +188 -0
  93. data/.ai-assistance/scripts/setup-mcps.ts +234 -0
  94. data/.ai-assistance/scripts/task-complete.ts +74 -0
  95. data/.ai-assistance/scripts/task-create.ts +75 -0
  96. data/.ai-assistance/scripts/task-read.ts +125 -0
  97. data/.ai-assistance/scripts/token-logger.js +220 -0
  98. data/.ai-assistance/scripts/token-report.ts +158 -0
  99. data/.ai-assistance/scripts/token-session-start.js +66 -0
  100. data/.ai-assistance/skills/ai-instructions/SKILL.md +48 -0
  101. data/.ai-assistance/skills/code-specs/SKILL.md +69 -0
  102. data/.ai-assistance/skills/corporate-policies/SKILL.md +201 -0
  103. data/.ai-assistance/skills/dep-graph/SKILL.md +139 -0
  104. data/.ai-assistance/skills/ep-ai-manager/SKILL.md +417 -0
  105. data/.ai-assistance/skills/gemini-assist/SKILL.md +63 -0
  106. data/.ai-assistance/skills/gemini-assist/gemini-mcp-server.js +205 -0
  107. data/.ai-assistance/skills/gemini-assist/gemini_ask.py +1 -0
  108. data/.ai-assistance/skills/gemini-assist/gemini_ask.rb +240 -0
  109. data/.ai-assistance/skills/gemini-assist/prompts/cycle_end_review.txt +25 -0
  110. data/.ai-assistance/skills/graphql-schema-analysis/SKILL.md +261 -0
  111. data/.ai-assistance/skills/project-cycle/SKILL.md +177 -0
  112. data/.ai-assistance/skills/refactor/SKILL.md +62 -0
  113. data/.ai-assistance/skills/rubocop/SKILL.md +93 -0
  114. data/.ai-assistance/skills/ruby-scripting/SKILL.md +215 -0
  115. data/.ai-assistance/skills/spec-generation/SKILL.md +72 -0
  116. data/.ai-assistance/standards-version.json +21 -0
  117. data/.ai-assistance/token-budget.json +32 -0
  118. data/.ai-assistance/version.json +39 -0
  119. data/.claude/settings.json +146 -0
  120. data/.env.example +18 -0
  121. data/.gitattributes +15 -0
  122. data/.gitignore +13 -0
  123. data/.rubocop.yml +121 -97
  124. data/CHANGELOG.md +673 -477
  125. data/CLAUDE.md +232 -0
  126. data/Gemfile +30 -6
  127. data/Rakefile +90 -38
  128. data/docs/worklog.md +574 -0
  129. data/ecoportal-api-graphql.gemspec +40 -40
  130. data/lib/ecoportal/api/common/graphql/CLAUDE.md +36 -0
  131. data/lib/ecoportal/api/common/graphql/client.rb +1 -1
  132. data/lib/ecoportal/api/common/graphql/http_client.rb +35 -3
  133. data/lib/ecoportal/api/common/graphql/model/CLAUDE.md +28 -0
  134. data/lib/ecoportal/api/common/graphql/model/as_input.rb +8 -5
  135. data/lib/ecoportal/api/common/graphql/model/diffable/classic_diff_service.rb +3 -1
  136. data/lib/ecoportal/api/common/graphql/model/diffable/diff_service.rb +31 -23
  137. data/lib/ecoportal/api/common/graphql/model/diffable/hash_diff_nesting.rb +54 -1
  138. data/lib/ecoportal/api/graphql/CLAUDE.md +37 -0
  139. data/lib/ecoportal/api/graphql/base/CLAUDE.md +50 -0
  140. data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +17 -0
  141. data/lib/ecoportal/api/graphql/base/delta_result.rb +16 -0
  142. data/lib/ecoportal/api/graphql/base/force/binding.rb +19 -0
  143. data/lib/ecoportal/api/graphql/base/force/binding_collection.rb +62 -0
  144. data/lib/ecoportal/api/graphql/base/force/collection.rb +47 -0
  145. data/lib/ecoportal/api/graphql/base/force.rb +65 -0
  146. data/lib/ecoportal/api/graphql/base/kickstand/job.rb +17 -0
  147. data/lib/ecoportal/api/graphql/base/kickstand/workflow.rb +18 -0
  148. data/lib/ecoportal/api/graphql/base/kickstand.rb +13 -0
  149. data/lib/ecoportal/api/graphql/base/page/basic.rb +38 -0
  150. data/lib/ecoportal/api/graphql/base/page/data_field/actions_list.rb +9 -0
  151. data/lib/ecoportal/api/graphql/base/page/data_field/ai_summary.rb +18 -0
  152. data/lib/ecoportal/api/graphql/base/page/data_field/checklist.rb +29 -0
  153. data/lib/ecoportal/api/graphql/base/page/data_field/collection.rb +112 -0
  154. data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +28 -0
  155. data/lib/ecoportal/api/graphql/base/page/data_field/cross_reference.rb +54 -0
  156. data/lib/ecoportal/api/graphql/base/page/data_field/date_field.rb +23 -0
  157. data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +25 -0
  158. data/lib/ecoportal/api/graphql/base/page/data_field/gauge.rb +19 -0
  159. data/lib/ecoportal/api/graphql/base/page/data_field/geo.rb +24 -0
  160. data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +24 -0
  161. data/lib/ecoportal/api/graphql/base/page/data_field/law.rb +8 -0
  162. data/lib/ecoportal/api/graphql/base/page/data_field/mailbox.rb +9 -0
  163. data/lib/ecoportal/api/graphql/base/page/data_field/number.rb +20 -0
  164. data/lib/ecoportal/api/graphql/base/page/data_field/people.rb +35 -0
  165. data/lib/ecoportal/api/graphql/base/page/data_field/plain_text.rb +17 -0
  166. data/lib/ecoportal/api/graphql/base/page/data_field/rich_text.rb +26 -0
  167. data/lib/ecoportal/api/graphql/base/page/data_field/select.rb +41 -0
  168. data/lib/ecoportal/api/graphql/base/page/data_field/signature.rb +9 -0
  169. data/lib/ecoportal/api/graphql/base/page/data_field/smart_fill.rb +17 -0
  170. data/lib/ecoportal/api/graphql/base/page/data_field/table.rb +9 -0
  171. data/lib/ecoportal/api/graphql/base/page/data_field/tag_field.rb +21 -0
  172. data/lib/ecoportal/api/graphql/base/page/data_field.rb +137 -12
  173. data/lib/ecoportal/api/graphql/base/page/phased/stage.rb +68 -14
  174. data/lib/ecoportal/api/graphql/base/page/phased.rb +1 -0
  175. data/lib/ecoportal/api/graphql/base/page/section.rb +36 -0
  176. data/lib/ecoportal/api/graphql/base/page/section_collection.rb +79 -0
  177. data/lib/ecoportal/api/graphql/base/page.rb +2 -0
  178. data/lib/ecoportal/api/graphql/base/pages_workflow/action_type_selection.rb +17 -0
  179. data/lib/ecoportal/api/graphql/base/pages_workflow/callback_type.rb +28 -0
  180. data/lib/ecoportal/api/graphql/base/pages_workflow/command_change.rb +17 -0
  181. data/lib/ecoportal/api/graphql/base/pages_workflow/command_change_message.rb +15 -0
  182. data/lib/ecoportal/api/graphql/base/pages_workflow/command_es_change.rb +17 -0
  183. data/lib/ecoportal/api/graphql/base/pages_workflow/command_interface.rb +36 -0
  184. data/lib/ecoportal/api/graphql/base/pages_workflow/email_config.rb +18 -0
  185. data/lib/ecoportal/api/graphql/base/pages_workflow/escalation_level.rb +19 -0
  186. data/lib/ecoportal/api/graphql/base/pages_workflow/in_system_config.rb +16 -0
  187. data/lib/ecoportal/api/graphql/base/pages_workflow/mailbox_field_selection.rb +17 -0
  188. data/lib/ecoportal/api/graphql/base/pages_workflow/operation_interface.rb +39 -0
  189. data/lib/ecoportal/api/graphql/base/pages_workflow/operations/assign_to.rb +27 -0
  190. data/lib/ecoportal/api/graphql/base/pages_workflow/operations/create_page.rb +21 -0
  191. data/lib/ecoportal/api/graphql/base/pages_workflow/operations/send_notification.rb +30 -0
  192. data/lib/ecoportal/api/graphql/base/pages_workflow/people_field_selection.rb +17 -0
  193. data/lib/ecoportal/api/graphql/base/pages_workflow/recipient_config.rb +34 -0
  194. data/lib/ecoportal/api/graphql/base/pages_workflow/register_field.rb +17 -0
  195. data/lib/ecoportal/api/graphql/base/pages_workflow/task_config_selection.rb +17 -0
  196. data/lib/ecoportal/api/graphql/base/pages_workflow/time_delay_config.rb +16 -0
  197. data/lib/ecoportal/api/graphql/base/pages_workflow/trigger_interface.rb +26 -0
  198. data/lib/ecoportal/api/graphql/base/pages_workflow/triggers/conditional_logic.rb +17 -0
  199. data/lib/ecoportal/api/graphql/base/pages_workflow/user_selection.rb +16 -0
  200. data/lib/ecoportal/api/graphql/base/pages_workflow.rb +35 -0
  201. data/lib/ecoportal/api/graphql/base/preset_view.rb +17 -0
  202. data/lib/ecoportal/api/graphql/base/preview_page.rb +23 -0
  203. data/lib/ecoportal/api/graphql/base/register.rb +18 -0
  204. data/lib/ecoportal/api/graphql/base.rb +11 -0
  205. data/lib/ecoportal/api/graphql/builder/CLAUDE.md +65 -0
  206. data/lib/ecoportal/api/graphql/builder/kickstand.rb +73 -0
  207. data/lib/ecoportal/api/graphql/builder/page.rb +210 -41
  208. data/lib/ecoportal/api/graphql/builder/register/preset_view.rb +84 -0
  209. data/lib/ecoportal/api/graphql/builder/register.rb +27 -19
  210. data/lib/ecoportal/api/graphql/builder/template.rb +80 -0
  211. data/lib/ecoportal/api/graphql/builder.rb +2 -0
  212. data/lib/ecoportal/api/graphql/compat/filter_translator.rb +107 -0
  213. data/lib/ecoportal/api/graphql/compat/page_reference.rb +23 -0
  214. data/lib/ecoportal/api/graphql/compat/pages.rb +212 -0
  215. data/lib/ecoportal/api/graphql/compat/registers.rb +84 -0
  216. data/lib/ecoportal/api/graphql/compat/response.rb +35 -0
  217. data/lib/ecoportal/api/graphql/compat/search_results.rb +33 -0
  218. data/lib/ecoportal/api/graphql/compat/stage_collection.rb +70 -0
  219. data/lib/ecoportal/api/graphql/compat/stage_view.rb +76 -0
  220. data/lib/ecoportal/api/graphql/compat.rb +17 -0
  221. data/lib/ecoportal/api/graphql/concerns/data_field_access.rb +71 -0
  222. data/lib/ecoportal/api/graphql/concerns/deprecation.rb +20 -0
  223. data/lib/ecoportal/api/graphql/concerns/fragment_definitions.rb +21 -28
  224. data/lib/ecoportal/api/graphql/concerns/page_compat.rb +51 -0
  225. data/lib/ecoportal/api/graphql/concerns/snake_camel_access.rb +60 -0
  226. data/lib/ecoportal/api/graphql/concerns.rb +4 -0
  227. data/lib/ecoportal/api/graphql/connection/page.rb +11 -0
  228. data/lib/ecoportal/api/graphql/connection/pages_workflow_command.rb +13 -0
  229. data/lib/ecoportal/api/graphql/connection/preset_view.rb +11 -0
  230. data/lib/ecoportal/api/graphql/connection/preview_page.rb +11 -0
  231. data/lib/ecoportal/api/graphql/connection.rb +4 -0
  232. data/lib/ecoportal/api/graphql/file_upload/client.rb +181 -0
  233. data/lib/ecoportal/api/graphql/file_upload.rb +10 -0
  234. data/lib/ecoportal/api/graphql/fragment/action.rb +1 -1
  235. data/lib/ecoportal/api/graphql/fragment/action_category.rb +1 -1
  236. data/lib/ecoportal/api/graphql/fragment/contractor_entity.rb +1 -1
  237. data/lib/ecoportal/api/graphql/fragment/force.rb +30 -0
  238. data/lib/ecoportal/api/graphql/fragment/location_draft.rb +2 -2
  239. data/lib/ecoportal/api/graphql/fragment/location_node.rb +1 -1
  240. data/lib/ecoportal/api/graphql/fragment/locations_error.rb +1 -1
  241. data/lib/ecoportal/api/graphql/fragment/page.rb +85 -0
  242. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +395 -0
  243. data/lib/ecoportal/api/graphql/fragment/pages.rb +15 -0
  244. data/lib/ecoportal/api/graphql/fragment/pages_workflow.rb +172 -0
  245. data/lib/ecoportal/api/graphql/fragment/pagination.rb +1 -1
  246. data/lib/ecoportal/api/graphql/fragment.rb +37 -27
  247. data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +25 -0
  248. data/lib/ecoportal/api/graphql/input/delta_input.rb +16 -0
  249. data/lib/ecoportal/api/graphql/input/page/archive.rb +14 -0
  250. data/lib/ecoportal/api/graphql/input/page/build_from_template.rb +13 -0
  251. data/lib/ecoportal/api/graphql/input/page/create_draft.rb +13 -0
  252. data/lib/ecoportal/api/graphql/input/page/create_from_template.rb +18 -0
  253. data/lib/ecoportal/api/graphql/input/page/delete_draft.rb +13 -0
  254. data/lib/ecoportal/api/graphql/input/page/publish_draft.rb +13 -0
  255. data/lib/ecoportal/api/graphql/input/page/review_task.rb +14 -0
  256. data/lib/ecoportal/api/graphql/input/page/unarchive.rb +14 -0
  257. data/lib/ecoportal/api/graphql/input/page/update.rb +140 -0
  258. data/lib/ecoportal/api/graphql/input/page.rb +26 -0
  259. data/lib/ecoportal/api/graphql/input/preset_view/create.rb +18 -0
  260. data/lib/ecoportal/api/graphql/input/preset_view/permission.rb +16 -0
  261. data/lib/ecoportal/api/graphql/input/preset_view/update.rb +16 -0
  262. data/lib/ecoportal/api/graphql/input/preset_view.rb +14 -0
  263. data/lib/ecoportal/api/graphql/input/register/create.rb +18 -0
  264. data/lib/ecoportal/api/graphql/input/register/update.rb +15 -0
  265. data/lib/ecoportal/api/graphql/input/register.rb +13 -0
  266. data/lib/ecoportal/api/graphql/input/search_conf/ai_generator.rb +234 -0
  267. data/lib/ecoportal/api/graphql/input/search_conf.rb +367 -0
  268. data/lib/ecoportal/api/graphql/input/variable_binding.rb +20 -0
  269. data/lib/ecoportal/api/graphql/input/workflow_command/add_action_tag.rb +18 -0
  270. data/lib/ecoportal/api/graphql/input/workflow_command/add_binding.rb +18 -0
  271. data/lib/ecoportal/api/graphql/input/workflow_command/add_comment_tagging_user_group.rb +18 -0
  272. data/lib/ecoportal/api/graphql/input/workflow_command/add_default_direct_strategy_user.rb +18 -0
  273. data/lib/ecoportal/api/graphql/input/workflow_command/add_default_strategy.rb +18 -0
  274. data/lib/ecoportal/api/graphql/input/workflow_command/add_direct_strategy_user.rb +18 -0
  275. data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +18 -0
  276. data/lib/ecoportal/api/graphql/input/workflow_command/add_force.rb +18 -0
  277. data/lib/ecoportal/api/graphql/input/workflow_command/add_gauge_field_stop.rb +19 -0
  278. data/lib/ecoportal/api/graphql/input/workflow_command/add_linked_field_config.rb +23 -0
  279. data/lib/ecoportal/api/graphql/input/workflow_command/add_linked_helper.rb +18 -0
  280. data/lib/ecoportal/api/graphql/input/workflow_command/add_operation.rb +18 -0
  281. data/lib/ecoportal/api/graphql/input/workflow_command/add_operation_direct_strategy_user.rb +18 -0
  282. data/lib/ecoportal/api/graphql/input/workflow_command/add_operation_strategy.rb +18 -0
  283. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_action_type.rb +18 -0
  284. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_filter.rb +18 -0
  285. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_people_field.rb +18 -0
  286. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_task_config.rb +18 -0
  287. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_user.rb +18 -0
  288. data/lib/ecoportal/api/graphql/input/workflow_command/add_scheduled_callback.rb +23 -0
  289. data/lib/ecoportal/api/graphql/input/workflow_command/add_scheduled_callback_action.rb +18 -0
  290. data/lib/ecoportal/api/graphql/input/workflow_command/add_section.rb +18 -0
  291. data/lib/ecoportal/api/graphql/input/workflow_command/add_select_field_option.rb +19 -0
  292. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage.rb +18 -0
  293. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage_section.rb +18 -0
  294. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage_tag.rb +18 -0
  295. data/lib/ecoportal/api/graphql/input/workflow_command/add_strategy.rb +18 -0
  296. data/lib/ecoportal/api/graphql/input/workflow_command/add_task.rb +18 -0
  297. data/lib/ecoportal/api/graphql/input/workflow_command/add_task_assignment_user_group.rb +18 -0
  298. data/lib/ecoportal/api/graphql/input/workflow_command/add_workflow_callback.rb +18 -0
  299. data/lib/ecoportal/api/graphql/input/workflow_command/collapse_section.rb +18 -0
  300. data/lib/ecoportal/api/graphql/input/workflow_command/edit_binding.rb +18 -0
  301. data/lib/ecoportal/api/graphql/input/workflow_command/edit_creator_permissions.rb +18 -0
  302. data/lib/ecoportal/api/graphql/input/workflow_command/edit_default_strategy.rb +18 -0
  303. data/lib/ecoportal/api/graphql/input/workflow_command/edit_field_configuration.rb +21 -0
  304. data/lib/ecoportal/api/graphql/input/workflow_command/edit_force.rb +18 -0
  305. data/lib/ecoportal/api/graphql/input/workflow_command/edit_gauge_field_stop.rb +19 -0
  306. data/lib/ecoportal/api/graphql/input/workflow_command/edit_linked_field_config.rb +19 -0
  307. data/lib/ecoportal/api/graphql/input/workflow_command/edit_linked_helper.rb +18 -0
  308. data/lib/ecoportal/api/graphql/input/workflow_command/edit_operation.rb +18 -0
  309. data/lib/ecoportal/api/graphql/input/workflow_command/edit_operation_strategy.rb +18 -0
  310. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page.rb +28 -0
  311. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page_creator_permissions.rb +18 -0
  312. data/lib/ecoportal/api/graphql/input/workflow_command/edit_reminder.rb +18 -0
  313. data/lib/ecoportal/api/graphql/input/workflow_command/edit_required_sign_offs.rb +18 -0
  314. data/lib/ecoportal/api/graphql/input/workflow_command/edit_restrict_comment_tagging.rb +18 -0
  315. data/lib/ecoportal/api/graphql/input/workflow_command/edit_restrict_task_assignment.rb +18 -0
  316. data/lib/ecoportal/api/graphql/input/workflow_command/edit_scheduled_callback.rb +22 -0
  317. data/lib/ecoportal/api/graphql/input/workflow_command/edit_section_header.rb +18 -0
  318. data/lib/ecoportal/api/graphql/input/workflow_command/edit_select_field_option.rb +19 -0
  319. data/lib/ecoportal/api/graphql/input/workflow_command/edit_stage.rb +18 -0
  320. data/lib/ecoportal/api/graphql/input/workflow_command/edit_strategy.rb +18 -0
  321. data/lib/ecoportal/api/graphql/input/workflow_command/edit_task_due.rb +18 -0
  322. data/lib/ecoportal/api/graphql/input/workflow_command/edit_trigger.rb +18 -0
  323. data/lib/ecoportal/api/graphql/input/workflow_command/expand_section.rb +18 -0
  324. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/contractor_entities.rb +24 -0
  325. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/cross_reference.rb +23 -0
  326. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/date.rb +20 -0
  327. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/gauge.rb +20 -0
  328. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/image_gallery.rb +20 -0
  329. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/location_field.rb +24 -0
  330. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/people.rb +24 -0
  331. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/plain_text.rb +20 -0
  332. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/rich_text.rb +20 -0
  333. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/select.rb +20 -0
  334. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/signature.rb +20 -0
  335. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/table.rb +25 -0
  336. data/lib/ecoportal/api/graphql/input/workflow_command/move_field.rb +18 -0
  337. data/lib/ecoportal/api/graphql/input/workflow_command/move_stage.rb +18 -0
  338. data/lib/ecoportal/api/graphql/input/workflow_command/remove_action_tag.rb +18 -0
  339. data/lib/ecoportal/api/graphql/input/workflow_command/remove_binding.rb +18 -0
  340. data/lib/ecoportal/api/graphql/input/workflow_command/remove_callback.rb +18 -0
  341. data/lib/ecoportal/api/graphql/input/workflow_command/remove_comment_tagging_user_group.rb +18 -0
  342. data/lib/ecoportal/api/graphql/input/workflow_command/remove_default_direct_strategy_user.rb +18 -0
  343. data/lib/ecoportal/api/graphql/input/workflow_command/remove_default_strategy.rb +18 -0
  344. data/lib/ecoportal/api/graphql/input/workflow_command/remove_direct_strategy_user.rb +18 -0
  345. data/lib/ecoportal/api/graphql/input/workflow_command/remove_field.rb +18 -0
  346. data/lib/ecoportal/api/graphql/input/workflow_command/remove_force.rb +18 -0
  347. data/lib/ecoportal/api/graphql/input/workflow_command/remove_gauge_field_stop.rb +17 -0
  348. data/lib/ecoportal/api/graphql/input/workflow_command/remove_linked_field_config.rb +17 -0
  349. data/lib/ecoportal/api/graphql/input/workflow_command/remove_linked_helper.rb +18 -0
  350. data/lib/ecoportal/api/graphql/input/workflow_command/remove_operation.rb +18 -0
  351. data/lib/ecoportal/api/graphql/input/workflow_command/remove_operation_direct_strategy_user.rb +18 -0
  352. data/lib/ecoportal/api/graphql/input/workflow_command/remove_operation_strategy.rb +18 -0
  353. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_action_type.rb +18 -0
  354. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_filter.rb +18 -0
  355. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_people_field.rb +18 -0
  356. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_task_config.rb +18 -0
  357. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_user.rb +18 -0
  358. data/lib/ecoportal/api/graphql/input/workflow_command/remove_scheduled_callback.rb +18 -0
  359. data/lib/ecoportal/api/graphql/input/workflow_command/remove_section.rb +18 -0
  360. data/lib/ecoportal/api/graphql/input/workflow_command/remove_select_field_option.rb +17 -0
  361. data/lib/ecoportal/api/graphql/input/workflow_command/remove_stage.rb +18 -0
  362. data/lib/ecoportal/api/graphql/input/workflow_command/remove_stage_section.rb +18 -0
  363. data/lib/ecoportal/api/graphql/input/workflow_command/remove_stage_tag.rb +18 -0
  364. data/lib/ecoportal/api/graphql/input/workflow_command/remove_strategy.rb +18 -0
  365. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task.rb +18 -0
  366. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task_assignment_user_group.rb +18 -0
  367. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task_due.rb +18 -0
  368. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task_priority_level.rb +18 -0
  369. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_forces.rb +18 -0
  370. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_section.rb +18 -0
  371. data/lib/ecoportal/api/graphql/input/workflow_command.rb +251 -0
  372. data/lib/ecoportal/api/graphql/input.rb +8 -0
  373. data/lib/ecoportal/api/graphql/interface/base_page.rb +100 -58
  374. data/lib/ecoportal/api/graphql/interface/location_structure/nodes.rb +27 -28
  375. data/lib/ecoportal/api/graphql/logic/base_model.rb +2 -0
  376. data/lib/ecoportal/api/graphql/logic/base_query.rb +45 -2
  377. data/lib/ecoportal/api/graphql/logic/input.rb +15 -0
  378. data/lib/ecoportal/api/graphql/model/ai_summary_version.rb +10 -0
  379. data/lib/ecoportal/api/graphql/model/organization.rb +65 -55
  380. data/lib/ecoportal/api/graphql/model/page/basic.rb +14 -0
  381. data/lib/ecoportal/api/graphql/model/page/phased.rb +82 -20
  382. data/lib/ecoportal/api/graphql/model/page.rb +1 -0
  383. data/lib/ecoportal/api/graphql/model/page_union.rb +21 -0
  384. data/lib/ecoportal/api/graphql/model/pages_workflow.rb +20 -0
  385. data/lib/ecoportal/api/graphql/model/preset_view.rb +10 -0
  386. data/lib/ecoportal/api/graphql/model/preview_page.rb +10 -0
  387. data/lib/ecoportal/api/graphql/model/register.rb +10 -0
  388. data/lib/ecoportal/api/graphql/model.rb +8 -0
  389. data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +45 -0
  390. data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +40 -0
  391. data/lib/ecoportal/api/graphql/mutation/ai_summary.rb +13 -0
  392. data/lib/ecoportal/api/graphql/mutation/kickstand/bulk_update_jobs.rb +43 -0
  393. data/lib/ecoportal/api/graphql/mutation/kickstand/bulk_update_workflows.rb +43 -0
  394. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_job.rb +39 -0
  395. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +39 -0
  396. data/lib/ecoportal/api/graphql/mutation/kickstand/start_job.rb +39 -0
  397. data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +39 -0
  398. data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +39 -0
  399. data/lib/ecoportal/api/graphql/mutation/kickstand.rb +18 -0
  400. data/lib/ecoportal/api/graphql/mutation/location_structure/apply_commands.rb +3 -3
  401. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/add_commands.rb +2 -2
  402. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/create.rb +2 -2
  403. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/drop_bad_commands.rb +3 -3
  404. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/publish.rb +3 -3
  405. data/lib/ecoportal/api/graphql/mutation/page/approve_review_task.rb +40 -0
  406. data/lib/ecoportal/api/graphql/mutation/page/archive.rb +40 -0
  407. data/lib/ecoportal/api/graphql/mutation/page/batch_update_review_task.rb +40 -0
  408. data/lib/ecoportal/api/graphql/mutation/page/build_from_template.rb +50 -0
  409. data/lib/ecoportal/api/graphql/mutation/page/create_draft.rb +40 -0
  410. data/lib/ecoportal/api/graphql/mutation/page/create_from_template.rb +43 -0
  411. data/lib/ecoportal/api/graphql/mutation/page/delete_draft.rb +40 -0
  412. data/lib/ecoportal/api/graphql/mutation/page/execute_force_commands.rb +69 -0
  413. data/lib/ecoportal/api/graphql/mutation/page/execute_workflow_commands.rb +51 -0
  414. data/lib/ecoportal/api/graphql/mutation/page/publish_draft.rb +40 -0
  415. data/lib/ecoportal/api/graphql/mutation/page/reject_review_task.rb +40 -0
  416. data/lib/ecoportal/api/graphql/mutation/page/restart_review_task.rb +40 -0
  417. data/lib/ecoportal/api/graphql/mutation/page/unarchive.rb +40 -0
  418. data/lib/ecoportal/api/graphql/mutation/page/undo_review_task.rb +40 -0
  419. data/lib/ecoportal/api/graphql/mutation/page/update.rb +40 -0
  420. data/lib/ecoportal/api/graphql/mutation/page/update_variable_bindings.rb +44 -0
  421. data/lib/ecoportal/api/graphql/mutation/page.rb +28 -0
  422. data/lib/ecoportal/api/graphql/mutation/preset_view/create.rb +35 -0
  423. data/lib/ecoportal/api/graphql/mutation/preset_view/destroy.rb +35 -0
  424. data/lib/ecoportal/api/graphql/mutation/preset_view/permission.rb +37 -0
  425. data/lib/ecoportal/api/graphql/mutation/preset_view/update.rb +35 -0
  426. data/lib/ecoportal/api/graphql/mutation/preset_view.rb +15 -0
  427. data/lib/ecoportal/api/graphql/mutation/register/create.rb +35 -0
  428. data/lib/ecoportal/api/graphql/mutation/register/destroy.rb +35 -0
  429. data/lib/ecoportal/api/graphql/mutation/register/update.rb +35 -0
  430. data/lib/ecoportal/api/graphql/mutation/register.rb +14 -0
  431. data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +36 -0
  432. data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +40 -0
  433. data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +13 -0
  434. data/lib/ecoportal/api/graphql/mutation/template/create.rb +39 -0
  435. data/lib/ecoportal/api/graphql/mutation/template/create_related_page.rb +46 -0
  436. data/lib/ecoportal/api/graphql/mutation/template/destroy_related_page.rb +43 -0
  437. data/lib/ecoportal/api/graphql/mutation/template/publish.rb +39 -0
  438. data/lib/ecoportal/api/graphql/mutation/template/unpublish.rb +39 -0
  439. data/lib/ecoportal/api/graphql/mutation/template/update.rb +43 -0
  440. data/lib/ecoportal/api/graphql/mutation/template/update_information.rb +43 -0
  441. data/lib/ecoportal/api/graphql/mutation/template.rb +18 -0
  442. data/lib/ecoportal/api/graphql/mutation.rb +8 -0
  443. data/lib/ecoportal/api/graphql/payload/ai_summary_generate.rb +12 -0
  444. data/lib/ecoportal/api/graphql/payload/execute_workflow_commands.rb +36 -0
  445. data/lib/ecoportal/api/graphql/payload/force_commands.rb +31 -0
  446. data/lib/ecoportal/api/graphql/payload/kickstand/bulk_update_jobs.rb +36 -0
  447. data/lib/ecoportal/api/graphql/payload/kickstand/bulk_update_workflows.rb +36 -0
  448. data/lib/ecoportal/api/graphql/payload/kickstand/job.rb +13 -0
  449. data/lib/ecoportal/api/graphql/payload/kickstand/workflow.rb +13 -0
  450. data/lib/ecoportal/api/graphql/payload/kickstand.rb +15 -0
  451. data/lib/ecoportal/api/graphql/payload/location_structure/draft/create.rb +33 -34
  452. data/lib/ecoportal/api/graphql/payload/ok_payload.rb +21 -0
  453. data/lib/ecoportal/api/graphql/payload/page/archive.rb +13 -0
  454. data/lib/ecoportal/api/graphql/payload/page/build_from_template.rb +13 -0
  455. data/lib/ecoportal/api/graphql/payload/page/create_from_template.rb +13 -0
  456. data/lib/ecoportal/api/graphql/payload/page/draft.rb +13 -0
  457. data/lib/ecoportal/api/graphql/payload/page/review_task.rb +13 -0
  458. data/lib/ecoportal/api/graphql/payload/page/unarchive.rb +13 -0
  459. data/lib/ecoportal/api/graphql/payload/page/update.rb +13 -0
  460. data/lib/ecoportal/api/graphql/payload/page/update_variable_bindings.rb +13 -0
  461. data/lib/ecoportal/api/graphql/payload/page.rb +19 -0
  462. data/lib/ecoportal/api/graphql/payload/preset_view.rb +11 -0
  463. data/lib/ecoportal/api/graphql/payload/register.rb +11 -0
  464. data/lib/ecoportal/api/graphql/payload/template/create.rb +13 -0
  465. data/lib/ecoportal/api/graphql/payload/template/create_related_page.rb +13 -0
  466. data/lib/ecoportal/api/graphql/payload/template/destroy_related_page.rb +13 -0
  467. data/lib/ecoportal/api/graphql/payload/template/publish.rb +13 -0
  468. data/lib/ecoportal/api/graphql/payload/template/unpublish.rb +13 -0
  469. data/lib/ecoportal/api/graphql/payload/template/update.rb +13 -0
  470. data/lib/ecoportal/api/graphql/payload/template/update_information.rb +13 -0
  471. data/lib/ecoportal/api/graphql/payload/template.rb +18 -0
  472. data/lib/ecoportal/api/graphql/payload.rb +11 -0
  473. data/lib/ecoportal/api/graphql/query/action.rb +1 -1
  474. data/lib/ecoportal/api/graphql/query/action_categories.rb +1 -1
  475. data/lib/ecoportal/api/graphql/query/actions.rb +2 -2
  476. data/lib/ecoportal/api/graphql/query/contractor_entities.rb +1 -1
  477. data/lib/ecoportal/api/graphql/query/file_upload_signature.rb +76 -0
  478. data/lib/ecoportal/api/graphql/query/location_structure/draft.rb +2 -2
  479. data/lib/ecoportal/api/graphql/query/location_structure.rb +1 -1
  480. data/lib/ecoportal/api/graphql/query/location_structures.rb +1 -1
  481. data/lib/ecoportal/api/graphql/query/page.rb +45 -0
  482. data/lib/ecoportal/api/graphql/query/page_delta.rb +47 -0
  483. data/lib/ecoportal/api/graphql/query/page_with_forces.rb +43 -0
  484. data/lib/ecoportal/api/graphql/query/pages.rb +59 -0
  485. data/lib/ecoportal/api/graphql/query/pages_workflow_commands.rb +59 -0
  486. data/lib/ecoportal/api/graphql/query/register_preset_views.rb +78 -0
  487. data/lib/ecoportal/api/graphql/query/register_preview_pages.rb +83 -0
  488. data/lib/ecoportal/api/graphql/query/templates.rb +53 -0
  489. data/lib/ecoportal/api/graphql/query.rb +11 -0
  490. data/lib/ecoportal/api/graphql.rb +60 -2
  491. data/lib/ecoportal/api/graphql_version.rb +5 -5
  492. data/scripts/auto-worker-scheduler.sh +386 -0
  493. data/tests/contractor_entity_create.rb +19 -19
  494. data/tests/contractor_entity_udpate.rb +20 -20
  495. data/tests/dump_page_model.rb +74 -0
  496. data/tests/loc_structure_get.rb +1 -2
  497. data/tests/loc_structure_update.rb +51 -51
  498. data/tests/loc_structures_get.rb +15 -15
  499. metadata +436 -5
@@ -0,0 +1,256 @@
1
+ # TODOs — Ooze → Native GraphQL Migration
2
+
3
+ Legend: `[ ]` todo · `[~]` in progress · `[x]` done · `[!]` blocked (see Risks/DECISIONS)
4
+
5
+ Each phase ends with a gate. **Do not flip an old class name to delegate until that
6
+ case's parity result is recorded in DECISIONS.md.**
7
+
8
+ ---
9
+
10
+ ## Phase 0 — Inventory & Baseline (no code changes)
11
+
12
+ ### 0a. Map the full ooze case inheritance/include tree
13
+ - [ ] Confirm the inheritance chain (verified 2026-06-30, re-confirm before building):
14
+ - `Common::Loaders::UseCase`
15
+ - `OozeSamples::OozeBaseCase` — `include Helpers`, `include Helpers::Rescuable`
16
+ - `OozeSamples::OozeRunBaseCase` (single-ooze runner)
17
+ - `OozeSamples::OozeUpdateCase`
18
+ - `OozeSamples::OozeFromDocCase`
19
+ - `OozeSamples::RegisterUpdateCase` — `include Helpers::Creatable`
20
+ - `OozeSamples::TargetOozesUpdateCase` ← **first cutover case** (act-gov TOOCS)
21
+ - `OozeSamples::RegisterMigrationCase` — `include HelpersMigration`
22
+ - `OozeSamples::RegisterExportCase` (standalone — NOT under OozeBaseCase)
23
+ - `OozeCases::ExportRegisterCase` — `include Helpers::ExportableRegister`
24
+ - [ ] Map the helper module surface each case relies on:
25
+ - `OozeSamples::Helpers` aggregate → `Shortcuts`, `Filters`, `OozeHandlers`,
26
+ `Creatable`, `Rescuable`, `ExportableOoze`, `ExportableRegister`
27
+ - `OozeSamples::HelpersMigration` → `Copying`, `TypedFieldsPairing`
28
+ - [ ] For each helper, classify: **v2-coupled** (references
29
+ `Ecoportal::API::V2::Page::*`, e.g. `OozeHandlers#merge_values`, `Shortcuts#object_reference`,
30
+ `Filters#field_key_name`) vs **pure** (string/array utils — `Shortcuts#same_string?`,
31
+ `simplify_string`, `OozeHandlers#merge_arrays`). Pure helpers port verbatim; v2-coupled
32
+ helpers need GraphQL-typed re-expression.
33
+ - [ ] Record which case methods are leaf v2 ops vs control flow (the line that the shim bug
34
+ blurred): control flow = `main`, `with_each_entry`, `update_ooze`, `update_oozes`,
35
+ `process_ooze`, `ooze`, `new_target`, the KPI counters, dry-run feedback. Leaf ops =
36
+ field/stage access, `apiv2.pages.{get,get_new,create,update,get_body}`,
37
+ `apiv2.registers.search`.
38
+
39
+ ### 0b. Inventory every downstream script include site (BC surface)
40
+ - [ ] In the training repo (`C:\ruby_scripts\implementation\training`) and any other
41
+ consumer repos, grep for the public names that scripts subclass/include:
42
+ - `OozeSamples::OozeBaseCase`, `OozeRunBaseCase`, `OozeUpdateCase`, `OozeFromDocCase`
43
+ - `OozeSamples::RegisterUpdateCase`, `TargetOozesUpdateCase`, `RegisterMigrationCase`
44
+ - `OozeSamples::RegisterExportCase`, `OozeCases::ExportRegisterCase`
45
+ - `OozeSamples::Helpers::Creatable` and any other helper included directly
46
+ - `GraphQL::Compat::OozeRedirect` (the include line scripts add today)
47
+ - [ ] Produce `INVENTORY.md` in this folder: per public name, list (a) every consumer
48
+ script that subclasses/includes it, (b) which overridable methods that script defines
49
+ (`process_ooze`, `search`, `filters`, `custom_processing`, `excluded_field_hooks`, etc.),
50
+ (c) whether the script touches forces (→ blocked partition).
51
+ - [ ] **Forces live readiness test (decides the partition below).** Do NOT assume forces are
52
+ blocked — the gem ships read (`Query::PageWithForces`) AND write
53
+ (`executeWorkflowCommands`: `editForce`/`addBinding`/`removeBinding`/`removeForce`) client
54
+ code, gated off via `OozeRedirect.force_support? == false`. Against a real org: (a) run
55
+ `PageWithForces` on a force-bearing page and confirm the query is schema-valid + returns
56
+ populated forces; (b) run a no-op `editForce`/`addBinding` via `executeWorkflowCommands` and
57
+ confirm it round-trips. Record the verdict — it determines whether forces are IN scope.
58
+ - [ ] **Partition the inventory into two tables: "migratable now" vs "blocked on forces"**
59
+ — gated on the readiness test above. If forces pass, there is NO blocked partition. If they
60
+ fail, seed the blocked list from `ooze_redirect.rb` lines 68–77 (act-gov, briscoes, chorus,
61
+ hcc, lic, mitre10, npdc, profile-group, turners-growers, twg). This partition is the
62
+ master schedule for Phases 2..N.
63
+
64
+ ### 0c. Capture the v2 baseline behaviour (parity reference)
65
+ - [ ] For each migratable case, run the existing v2 path under `-simulate` against the
66
+ `mini` test org and save the dry-run diff + KPI block as the golden reference
67
+ (`baselines/<case>.txt`). This is the A side of every later A/B comparison.
68
+ - [ ] Confirm the A/B parity harness exists / is buildable — see memory
69
+ `project-parity-test-plan` (all-21-field template in `mini`, A/B standalone harness in
70
+ e.g. `mini/graphql_parity/`, plus the eco-helpers session-backend swap). If the harness
71
+ is not yet built, that is a prerequisite sub-task here, not in Phase 2.
72
+
73
+ **Gate 0:** INVENTORY.md complete, partitioned, and baselines captured for the migratable set.
74
+
75
+ ---
76
+
77
+ ## Phase 1 — Extract shared concerns to `graphql/helpers/pages/*`
78
+
79
+ Goal: build the native shared substrate **before** any case, so case classes stay thin.
80
+ Mirror the existing native helper convention (`graphql/helpers/base/{case_env,graphql_env,
81
+ error_handling}.rb`, `graphql/samples/pages/page/base.rb`).
82
+
83
+ - [ ] `graphql/helpers/pages.rb` — namespace aggregator (mirrors `helpers/location.rb`).
84
+ - [ ] `graphql/helpers/pages/shortcuts.rb` — port the **pure** `OozeSamples::Helpers::Shortcuts`
85
+ methods (`same_string?`, `simplify_string`, `titleize`, `normalize_string`,
86
+ `clean_question`, `to_i`, `is_number?`). Re-express `object_reference` for GraphQL
87
+ page/section/component model types (drop the `Ecoportal::API::V2::*` `case/when`).
88
+ - [ ] `graphql/helpers/pages/filters.rb` — port `Filters`, re-expressing `field_key_name`
89
+ for GraphQL field types (`value.ref_backend` / `value.label` → GraphQL equivalents).
90
+ - [ ] `graphql/helpers/pages/ooze_handlers.rb` — re-express `merge_values` `case/when` against
91
+ `Base::Page::DataField::*` subclasses instead of `V2::Page::Component::*`; `merge_arrays`
92
+ is pure → port verbatim.
93
+ - [ ] `graphql/helpers/pages/rescuable.rb` — port `Rescuable` (validate base type ==
94
+ `GraphQL::Base` instead of `Common::Loaders::Base`).
95
+ - [ ] `graphql/helpers/pages/creatable.rb` — native `Creatable`: `creating_new_page`,
96
+ `drafting_entry` (→ `graphql.pages.get_new`), `create_entry`/`create_ooze`
97
+ (→ `graphql.pages.create`), with dry-run feedback. Base-type guard → native register case.
98
+ - [ ] `graphql/helpers/pages/patch_feedback.rb` — extract the dry-run/dirty machinery that
99
+ the shim bug exposed: `dirty?(page)` (`!as_update.nil?` OR pending submit/sign-off),
100
+ `display_patch` / `backup_patch!` reading `graphql.pages.get_body(page)['page']`,
101
+ `dry_run_feedback` + `DRY_COUNT` sampling. **This is the one-source-of-truth replacement
102
+ for the bug-prone shim-vs-baseloop split.**
103
+ - [ ] Spec each helper module in `spec/eco/api/usecases/graphql/helpers/pages/*_spec.rb`.
104
+ - [ ] `bundle exec rubocop` clean on all new helper files.
105
+
106
+ **Gate 1:** helper specs green; no production case yet depends on them (pure addition).
107
+
108
+ ---
109
+
110
+ ## Phase 2 — `RegisterUpdateCase` + `Helpers::Creatable` (jamestrong CANS upsert)
111
+
112
+ > Sequenced first alongside Phase 3 because both are exercised in the live cutover and
113
+ > `TargetOozesUpdateCase` **inherits** `RegisterUpdateCase`. Build the parent first.
114
+
115
+ - [ ] Build native `GraphQL::Samples::Pages::Register::Base` (working title) under
116
+ `graphql/samples/pages/register/base.rb`, modelled on the existing
117
+ `graphql/samples/pages/page/base.rb` (KPIs, `each_page`, `update_page`, `simulate?` guard)
118
+ but reproducing the **RegisterUpdateCase** semantics faithfully:
119
+ - batched search via `graphql.registers.search` / `graphql.pages.each(searchConf:)`
120
+ - dedup by page id (`ooze_result_ids`), queue/`enqueue`/`queue_shift` batching
121
+ - full KPI set: total_search / retrieved / non_retrieved / dupped / updated /
122
+ failed_update / attempted_updates / created / create_attempts (match `kpis_message`)
123
+ - `before_loading_new_target` flush-on-requeue behaviour
124
+ - `results_preview` + interactive `prompt_user` proceed (y/N) (v2-faithful)
125
+ - [ ] `include GraphQL::Helpers::Pages::Creatable` + `PatchFeedback` + `Shortcuts`.
126
+ - [ ] **Specs first:** `spec/eco/api/usecases/graphql/samples/pages/register/base_spec.rb`
127
+ — cover KPI counting, dedup, dry-run diff printing, create path, submit/sign-off fold.
128
+ Port the assertions from `spec/.../compat/pages_spec.rb` regression specs where relevant.
129
+ - [ ] **Parity-prove** against the v2 `RegisterUpdateCase` baseline using the A/B harness on
130
+ `mini` (read-only first, then a 1-row CANS-style upsert). Record result in DECISIONS.md.
131
+ - [ ] **Flip the old name:** make
132
+ `Eco::API::UseCases::OozeSamples::RegisterUpdateCase` a thin subclass/delegator of the
133
+ native class (preserve `name 'register-update-case'`, `batch_size`, attr_readers,
134
+ `process_ooze` override point, `main(..., mode: :legacy|:delegate)` signature).
135
+ `OozeSamples::Helpers::Creatable` → delegate to / alias the native `Creatable`
136
+ (keep `validate_base_type!` accepting the native register base).
137
+ - [ ] **Remove shim code for this case:** delete the redirect paths now dead for register
138
+ cases. (Keep `FieldPatches` until the *last* case that needs them is flipped — track in
139
+ DECISIONS.md which patches each flip retires.)
140
+ - [ ] Re-run jamestrong CANS script **unchanged** on `mini`; confirm parity with baseline.
141
+
142
+ **Gate 2:** CANS parity recorded; old `RegisterUpdateCase` name resolves to native; eco-helpers suite green.
143
+
144
+ ---
145
+
146
+ ## Phase 3 — `TargetOozesUpdateCase` (act-gov TOOCS coding)
147
+
148
+ - [ ] Build native counterpart subclassing the Phase-2 native register base, reproducing
149
+ `TargetOozesUpdateCase`: CSV `target_ids` source, `batched_target_ids`,
150
+ `target_ids_preview` (duplicate detection + proceed prompt), `with_each_entry` over ids.
151
+ - [ ] **Specs first:** id-batching, duplicate detection, requeue-flush, KPI counts,
152
+ cross-reference set + submit (the TOOCS coding shape), dry-run diff.
153
+ - [ ] **Parity-prove** against the v2 `TargetOozesUpdateCase` baseline (act-gov TOOCS,
154
+ 1-row CSV) on `mini`. Record in DECISIONS.md.
155
+ - [ ] **Flip the old name** `OozeSamples::TargetOozesUpdateCase` → native subclass.
156
+ - [ ] Remove now-dead shim code for this case.
157
+ - [ ] Re-run act-gov TOOCS script **unchanged**; confirm parity.
158
+
159
+ **Gate 3:** TOOCS parity recorded; old name resolves to native; both live cutover scripts green on native.
160
+
161
+ ---
162
+
163
+ ## Phase 4 — `OozeBaseCase` / `OozeRunBaseCase` / `OozeUpdateCase` / `OozeFromDocCase` (single-ooze chain)
164
+
165
+ > Single-ooze runners (`options[:source][:ooze_id]`). Lower live-traffic but needed for
166
+ > completeness and as the base several scripts subclass directly.
167
+
168
+ - [ ] Native `GraphQL::Samples::Pages::Ooze::Base` reproducing `OozeBaseCase`
169
+ (target, `process_ooze` block contract, `new_page`/`create_page`, `with_fields`/
170
+ `with_sections`/`with_stage`, dry-run feedback via `PatchFeedback`).
171
+ - [ ] Native single-ooze runner (`OozeRunBaseCase`: `ooze_id`/`stage_id` from options,
172
+ `exit_if_no_changes!`, `prompt_to_confirm!`).
173
+ - [ ] Native `OozeUpdateCase` (`to_field` resolver — drop the v2 `Component`/`Binding`
174
+ branches; resolve via GraphQL `components.get_by_id` / name match).
175
+ - [ ] Native `OozeFromDocCase` (docx table reading is v2-agnostic — port; only the field
176
+ access underneath changes).
177
+ - [ ] Specs for each; parity-prove each single-ooze runner on `mini`.
178
+ - [ ] Flip names `OozeBaseCase` → `OozeRunBaseCase` → `OozeUpdateCase` → `OozeFromDocCase`
179
+ bottom-up; remove dead shim code per flip.
180
+
181
+ **Gate 4:** single-ooze chain on native; names preserved; specs + parity recorded.
182
+
183
+ ---
184
+
185
+ ## Phase 5 — `RegisterMigrationCase` (+ `HelpersMigration`)
186
+
187
+ > Heaviest helper surface (`copy_pairings`, `copy_generic_paired_fields`,
188
+ > `copy_mapped_fields`, typed-field pairing). Confirm none of the target customer
189
+ > migrations rely on forces before scheduling (Phase 0 partition decides this).
190
+
191
+ - [ ] Port `HelpersMigration::{Copying,TypedFieldsPairing}` to
192
+ `graphql/helpers/pages/migration/*`, re-expressing typed-field copy against GraphQL
193
+ `DataField::*` types (the v2 `Component` type matrix → GraphQL type matrix).
194
+ - [ ] Native `RegisterMigrationCase` subclassing the native register base; reproduce the
195
+ field-maps json loading, `custom_processing`/block contract, `report_*` pairing reports.
196
+ - [ ] Specs (typed copy correctness is the high-risk area — cover each field type's copy).
197
+ - [ ] Parity-prove a real migration on `mini` (src→dst page, diff field-by-field via the
198
+ 21-type matrix from the parity plan).
199
+ - [ ] Flip name; remove dead shim code.
200
+
201
+ **Gate 5:** migration parity recorded (field-type matrix); name preserved.
202
+
203
+ ---
204
+
205
+ ## Phase 6 — Export cases (`RegisterExportCase`, `OozeCases::ExportRegisterCase`)
206
+
207
+ > Read-only (no update path) → safest. `RegisterExportCase` is **not** under `OozeBaseCase`
208
+ > and has its own `ooze`/`stage`/`apiv2` — note `build_full_ooze` stitches all stages into
209
+ > one `PageStage` (a v2-specific merge GraphQL gets for free by fetching the full page).
210
+
211
+ - [ ] Native `GraphQL::Samples::Pages::Register::Export` reproducing `RegisterExportCase`
212
+ (batched search, `build_full_ooze` → simply the full GraphQL PageUnion, no stage stitch).
213
+ - [ ] Port `Helpers::ExportableRegister` / `ExportableOoze` to native (CSV header/values
214
+ generation over GraphQL fields; honour `only_indexed`/`only_labeled`/`only_with_ref`).
215
+ - [ ] Native `ExportRegisterCase` (tags/date-range filters via native `Filters`).
216
+ - [ ] Specs (CSV header + row parity against v2 export of the same register on `mini`).
217
+ - [ ] Flip names; remove dead shim code.
218
+
219
+ **Gate 6:** export CSV byte-parity (or documented intentional diffs) on `mini`.
220
+
221
+ ---
222
+
223
+ ## Phase 7 — Retire the OozeRedirect shim entirely
224
+
225
+ - [ ] Confirm every **migratable** case is flipped and no migratable consumer still
226
+ `include`s `GraphQL::Compat::OozeRedirect`.
227
+ - [ ] Delete `lib/eco/api/usecases/graphql/compat/ooze_redirect.rb` and the
228
+ `ooze_redirect/{dirty_array,field_patches}.rb` parts **for all retired patches**.
229
+ - [ ] **Keep** `ooze_redirect/force_compat.rb` + the minimal include path **iff** any
230
+ force-blocked case still rides the v2 path and needs it. Otherwise delete too.
231
+ - [ ] Remove the `FieldPatches.apply!` global v2-class reopening (`patch_v2_type_dispatch!`)
232
+ and GraphQL field `prepend`s — these only existed to make v2 `case/when` dispatch work;
233
+ native cases dispatch on GraphQL types directly.
234
+ - [ ] Verify **no customer script edits were needed** against the Phase 0 inventory; any
235
+ script that still references a removed symbol is a defect to fix in this project, not a
236
+ customer change.
237
+ - [ ] Full `bundle exec rspec` green in eco-helpers; gem suite green; rubocop clean.
238
+ - [ ] Update `eco-helpers` CHANGELOG; update memory + this project's status → complete.
239
+
240
+ **Final gate:** shim deleted (modulo the explicitly-documented forces holdout), all names
241
+ preserved, zero script edits, suites green.
242
+
243
+ ---
244
+
245
+ ## Standing constraints (apply in every phase)
246
+
247
+ - [ ] Specs-first for each native class (parity-proving) — never flip a name before its
248
+ spec suite and recorded parity result exist.
249
+ - [ ] Native class is the **real implementation**; old `OozeSamples::*` name is a thin
250
+ subclass/delegator. **Never reopen/monkeypatch a class to migrate it.**
251
+ - [ ] Preserve every public class name, `name '...'` registration, `type :other`,
252
+ class-level `batch_size`/`register_id`, attr_readers, and documented override points.
253
+ - [ ] Each flip removes the shim code it makes dead — but only retire a `FieldPatch` once
254
+ the last case needing it is flipped (track in DECISIONS.md).
255
+ - [ ] Follow the working-tree safety protocol
256
+ (`.ai-assistance/conventions/code-working-tree-protocol.md`) for every eco-helpers change.
@@ -0,0 +1,122 @@
1
+ # Deep review — TOOCS + CANS cutover workflow (2026-06-30)
2
+
3
+ Exhaustive pre-live audit of the two production dry-runs (act-gov TOOCS coding, jamestrong
4
+ CANS upsert) on branch `fix/ooze-redirect-faithful-dryrun`. Goal: trust nothing that "looks
5
+ clean", enumerate every hypothesis (however unlikely), prove/disprove each with code evidence,
6
+ fix what's real, and harden what's latent. Four parallel sub-investigations fed this; verdicts
7
+ and fixes below. All gem fixes are committed with specs; full suite 465/0/2 pending, rubocop clean.
8
+
9
+ ## The two dry-run bodies under audit
10
+
11
+ TOOCS (after the stageId fix — now correct):
12
+ ```
13
+ {:id=>"6a3225532b97b00073c23aa8", :patchVer=>229,
14
+ :stageId=>"6a3225522b97b00073c22d7d", :submit=>true,
15
+ :task=>{:completePageTask=>{:forcedComplete=>true}},
16
+ :dataFields=>{:updates=>[{:people=>{:id=>"6a3225522b97b00073c22cea",
17
+ :peopleIds=>["65d6d3dcacb0b3000773f547"]}}]}}
18
+ ```
19
+ CANS (update-shaped body, but the entry was actually a CREATE — see H8/H-create):
20
+ ```
21
+ {:id=>"656e802f2d489c005ad0a6af", :patchVer=>22,
22
+ :dataFields=>{:updates=>[ 5×plainText, 2×number (one =0.0), 1×select ]}}
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Findings table (every hypothesis explored)
28
+
29
+ | # | Hypothesis | Verdict | Action |
30
+ |---|---|---|---|
31
+ | H1 | People `<<` REPLACES (wipes) existing people | **NOT a bug on normal path** — DirtyArray seeds from the read; backend is full-replace but gem sends the full set. | — |
32
+ | H1b | People reader has no fallback → wipe if read returns `peopleIds:null` while `people{id}` nodes exist | **LATENT risk** (real, not active under $content reads) | **FIXED** — reader falls back to `people` nodes |
33
+ | H2 | `peopleIds` is the wrong input key | **NOT a bug** — backend `PeopleInput.people_ids: [ID]`, flat array (not IdDiff). | — |
34
+ | H3 | Number empty CSV cell → `0.0` written over server value | **CONFIRMED footgun** — `"".to_f == 0.0`, no blank guard | **FIXED** — `value=` blank→nil |
35
+ | H4 | Select shape (all options w/ selected flags) wrong | **NOT a bug** — matches `OptionInput{id,selected}`; merge+deselect semantics; sending only-selected would fail to deselect | — |
36
+ | H5 | PlainText `""` clear is unintended | **NOT a bug** — schema String, real clear; dirty vs baseline | — |
37
+ | H6 | Phantom-dirty (PlainText/Number/Select) vs baseline | **NOT a bug** — diff uses `original_doc`; no read/write key asymmetry for these types | — |
38
+ | H7 | Value types wrong (Float/String/Boolean) | **NOT a bug** — match backend input types | — |
39
+ | H8 | CANS upsert mis-routes existing page → create (duplicate risk) | **NOT a bug** — routing by `search_by_id`; the entry genuinely was NOT found → correct CREATE; no duplicate risk | — |
40
+ | H9 | CANS KPIs lie ("Total 0 / Created out of 1 / Updated 0") | **NOT a bug** — numerically correct: 0 existing targets, 1 create attempt, dry-run so 0 created | — |
41
+ | H-create | CANS create dry-run previews as "would **update**" (wrong envelope: draft id+patchVer, no templateId) | **CONFIRMED display bug** — `new_draft?` only checked `patchVer.nil?`, but buildFromTemplate drafts carry a patchVer | **FIXED** — `_compat_source_template_id` marker |
42
+ | H10 | The new stageId guard breaks other ooze cases | **NOT a bug** — only TOOCS+CANS submit on the GraphQL path, both via `with_stage` → stage resolves; metlifecare is pure APIv2 | — |
43
+ | H11 | TOOCS recorded stageId is the wrong stage | **NOT a bug (by construction)** — StageView records the RESOLVED 'Coding' stage id; replay spec asserts it | — (verify id in UI) |
44
+
45
+ ---
46
+
47
+ ## Confirmed bugs + fixes (all gem, all spec-covered)
48
+
49
+ ### 1. Number blank → 0.0 (live-corruption footgun) — `base/page/data_field/number.rb`
50
+ `def value=(numeric); doc['value'] = numeric&.to_f; end` coerces `""` to `0.0` (Ruby `"".to_f`).
51
+ The CANS case assigns `fld.value = row[csv_header]&.strip` with no blank guard, so an empty CSV
52
+ numeric cell wrote a real `0.0`, and the diff (correctly) shipped it over the server value.
53
+ **Fix:** blank (nil / empty / whitespace string) → `nil`, never `0.0`; genuine numbers still
54
+ coerce to Float. Specs: 8 cases (string/Numeric/"0"/""/whitespace/nil/clear-over-value/no-op).
55
+ **Residual:** a non-numeric garbage string (`"abc"`) still `.to_f`→`0.0` — lesser concern, noted.
56
+ **Caller policy:** whether a blank should CLEAR vs be LEFT ALONE is the case's call (TOOCS skips
57
+ blanks for people; CANS does not for numbers) — the gem only guarantees blank ≠ 0.0.
58
+
59
+ ### 2. People reader has no node fallback (latent wipe) — `base/page/data_field/people.rb`
60
+ Reader read only `doc['peopleIds']`; if a response ever returned it null/empty while the
61
+ `people{id}` nodes were populated, `people_ids << x` would seed from `[]` and the full-replace
62
+ backend would WIPE the field. **Fix:** fall back to the `people` nodes when `peopleIds` is blank
63
+ (mirrors CrossReference's `references.nodes` fallback). Read key == write key, so no phantom-dirty.
64
+ Specs: fallback read, append-preserves-from-nodes, prefer-peopleIds-when-both-present.
65
+
66
+ ### 3. CANS create dry-run mislabel — `compat/pages.rb` + `interface/base_page.rb`
67
+ A `buildPageFromTemplate` draft carries a server patchVer, so `new_draft?` (patchVer-nil only)
68
+ classified it as an update → the create previewed as "would update" with a misleading envelope.
69
+ The read fragment fetches neither `draft` nor `sourceTemplateId`, so neither could be used.
70
+ **Fix:** `Compat::Pages#get_new` records `_compat_source_template_id` (new accessor on
71
+ `Interface::BasePage`); `new_draft?` treats that marker as the authoritative create signal;
72
+ `create_body` uses it for the templateId. Specs: get_new sets marker, marked-draft-with-patchVer
73
+ previews as create (templateId, no patchVer). Affects DRY-RUN ONLY (live create already correct).
74
+
75
+ ---
76
+
77
+ ## Verified-correct (no action)
78
+ - **People append preserves** existing people (DirtyArray seeds from read; gem sends full set;
79
+ backend `set_people_ids` is full-replace, so sending the full set is exactly right). `peopleIds`
80
+ is the correct flat `[ID]` key (backend `PeopleInput`).
81
+ - **Select** "all options with selected flags" is correct and is the only shape that reliably
82
+ DESELECTS (`SelectBuilder` merges by id; options not sent are untouched).
83
+ - **PlainText `""`** is a legitimate clear; **dirty** baselines on `original_doc` for all three
84
+ scalar/array types; **value types** match backend inputs.
85
+ - **CANS routing**: existing→update, missing→create, decided by `search_by_id`; no duplicate-create.
86
+ The KPIs are numerically correct. (The "would update" wording was the only defect — fixed.)
87
+ - **Guard blast radius**: only TOOCS + CANS submit on the GraphQL path; both via `with_stage`
88
+ (records the active stage) → guard satisfied. metlifecare `resident_events` is pure APIv2.
89
+
90
+ ## Backend evidence (from C:\docker\ecoPortal_master, via sub-investigations)
91
+ - `app/graphql/types/fill_in_page/inputs/data_fields/people_input.rb` → `people_ids: [ID]` flat.
92
+ - `app/services/new_ep/pages/assign_attributes_service.rb:175` → `set_people_ids` = full replace.
93
+ - `select_builder.rb` → merge-by-id; deselect requires sending `selected:false`.
94
+ - `{plain_text,number}_input.rb` → `String` / `Float` value types.
95
+
96
+ ---
97
+
98
+ ## Open questions — RESOLVED / remaining (updated 2026-07-01)
99
+ 1. **CANS field `...61` `value=>0.0` — RESOLVED: real zero, not an artifact.** The CANS CSV row
100
+ `FE15TR04715` has `STANDARD COST` = `0` (UNQUOTED literal) → `"0".to_f == 0.0`, correctly sent.
101
+ (`SITE="15"` → `…60 = 15.0`.) Confirmed against `QEST_Components_20260629.csv`. The Number fix
102
+ was never implicated in this value.
103
+ - **Eco::CSV empty-cell behavior (verified, `eco-helpers/lib/eco/csv.rb`):** `Eco::CSV.parse`
104
+ = `{headers: true, skip_blanks: true}` + stdlib `::CSV` with NO `nil_value`/`empty_value`
105
+ override → **unquoted** empty (`,,`) becomes `nil`; **quoted** empty (`""`) stays `""`. The
106
+ feed mixes both (e.g. `CUSTOMER_ITEM=""` → `""` → surfaced as `plainText value:""`). So a
107
+ quoted-empty NUMERIC cell WOULD hit `"".to_f → 0.0` — the Number/Gauge `blank→nil` fix is the
108
+ correct safety net even though this particular row didn't trigger it. ("empties → nil" only
109
+ holds for unquoted empties.) `skip_blanks` skips whole-blank ROWS, not empty cells.
110
+ - **Gauge had the identical footgun** (`numeric&.to_f`) — now fixed the same way. Both Number
111
+ and Gauge route through a single shared `DataField#numeric_or_nil` helper so they can't drift.
112
+ 2. **Created CANS pages** unsubmitted (first stage): parked — out of scope for now (Oscar).
113
+ 3. **TOOCS stage**: confirm `6a3225522b97b00073c22d7d` is the 'Coding' stage in act-gov (the
114
+ model dump tool can verify against the live page). Non-blocking.
115
+
116
+ ## Tooling delivered
117
+ - `tests/dump_page_model.rb` — pulls a page's GraphQL model (CommonPageUnion doc) to JSON,
118
+ before/after a submit, for capturing real fixtures + diffing what a submit changed.
119
+ - `spec/fixtures/pages/toocs_coding_page.json` — TOOCS page fixture (observed ids).
120
+ - `spec/ecoportal/api/graphql/integration/toocs_submit_replay_spec.rb` — replays the case flow,
121
+ asserts the EXACT observed body, and proves the regression (stageless submit raises; stage view
122
+ resolves it).
@@ -0,0 +1,148 @@
1
+ # RCA — "Forces are blocked on GraphQL" was wrong: forces are mutable via the Workflow* command-bus
2
+
3
+ **Date:** 2026-07-01
4
+ **Author:** retrospective analysis (AI agent), reviewed-by: pending (Oscar)
5
+ **Scope:** Why a clarified platform fact — *forces are created/edited/removed on GraphQL via `Workflow*` command types through `executeWorkflowCommands`* — was repeatedly missed, leaving a stale "blocked on forces" narrative in `eco-helpers` that disabled ~50% of ooze cases.
6
+ **Constraint:** analysis + recommendations only. No source code was changed.
7
+
8
+ ---
9
+
10
+ ## 1. Executive summary
11
+
12
+ **What was missed.** ecoPortal FORCES are fully mutable on GraphQL through the `WorkflowCommandInput` command-bus — `addForce` / `editForce` / `removeForce` / `reorderForces` / `addBinding` / `editBinding` / `removeBinding` — applied via the `executeWorkflowCommands` mutation. The gem has shipped these command inputs **and** the mutation client since 2026-06-10/11. There is **no** separate "forces endpoint" and there never needed to be one for *writes*.
13
+
14
+ **The one-line root cause.** The `OozeRedirect.force_support?` readiness gate keyed on the wrong signal — the presence of a **broken READ query** (`Query::PageWithForces`) — and when that signal flipped true it routed *every* ooze case through the broken read. The fix was to hard-disable the gate (`force_support? => false`), which threw out the **working MUTATE path** as collateral and froze a comment that over-generalises "the broken read" into "forces are NOT usable on GraphQL." The READ-vs-MUTATE distinction was never made explicit at the gate, so a read-only defect masqueraded as a total-capability block.
15
+
16
+ **Net effect.** `ooze_redirect.rb` simultaneously (a) ships a `ForceCompat` that *correctly* drives writes through `executeWorkflowCommands`, and (b) asserts in prose that "Forces are NOT usable on GraphQL yet … blocked on GraphQL endpoint," listing ~50% of ooze cases as blocked. The codebase contradicts itself.
17
+
18
+ ---
19
+
20
+ ## 2. The contradiction, with file:line evidence
21
+
22
+ ### Side A — "forces are blocked" (eco-helpers)
23
+
24
+ `C:\ruby_scripts\git\eco-helpers\lib\eco\api\usecases\graphql\compat\ooze_redirect.rb`
25
+
26
+ - **L42-49** — `## Limitations`: "Force / binding operations (`target.forces`, `force.bindings`) are not available in GraphQL — those calls will raise NoMethodError."
27
+ - **L51-77** — `## TODO: Force / binding support (blocked on GraphQL endpoint)`: "Engineering is working on exposing 'legacy forces' via the GraphQL API. Once that endpoint is available, extend this module with a `ForceCompat` sub-module …" — followed by a v2-shaped API sketch (`target.forces.get_by_name`, `force.bindings.add`, `force.custom_script = new_script`) and a roster of ~22 named cases across 10 orgs declared "currently blocked … ~50% of all ooze cases."
28
+ - **L113-122** — `force_support?`:
29
+ ```ruby
30
+ # Forces are NOT usable on GraphQL yet: Query::PageWithForces is a WIP whose query
31
+ # currently fails schema validation … and the backend forces endpoint is still in
32
+ # progress. … Keep OFF until forces actually work …
33
+ def self.force_support?
34
+ false
35
+ end
36
+ ```
37
+
38
+ ### Side B — "forces are mutable, right now" (the gem, already shipping)
39
+
40
+ - `lib\ecoportal\api\graphql\input\workflow_command.rb` **L106-117** — the `# Forces` block of `COMMAND_MAP` wires `addForce`, `removeForce`, `editForce`, `reorderForces`, `addBinding`, `editBinding`, `removeBinding` (+ `addLinkedHelper`/`editLinkedHelper`/`removeLinkedHelper`). These are first-class members of the command-bus, on the same footing as `addStage`, `addField`, etc.
41
+ - `lib\ecoportal\api\graphql\input\workflow_command\add_force.rb` — `VALID_KEYS = %i[placeholderId name customScript url contentB64]`, `SCHEMA_VERSION '20260605'`.
42
+ - `…\edit_force.rb` — `VALID_KEYS = %i[id name customScript url contentB64]`.
43
+ - `…\add_binding.rb` — `VALID_KEYS = %i[placeholderId forceId name referenceId type]`.
44
+ - `lib\ecoportal\api\graphql\mutation\page\execute_workflow_commands.rb` — `field_name :executeWorkflowCommands`; the generic command applier (`mutation(... commands: :WorkflowCommandInput!)`).
45
+ - `lib\ecoportal\api\graphql\mutation\page\execute_force_commands.rb` **L26-37** — `ExecuteForceCommands` is now a thin `@deprecated` alias that *delegates to the same `executeWorkflowCommands` mutation*. Its own doc-comment (L20-25) shows `{ editForce: {...} }`, `{ addBinding: {...} }`, `{ removeBinding: {...} }`, `{ removeForce: {...} }` as the command payloads.
46
+ - `lib\ecoportal\api\graphql\base\force.rb` **L33-60** — `Base::Force#custom_script=` queues `editForce`; `#all_pending_commands` emits `{ editForce: { id:, customScript: } }`; bindings queue `addBinding`/`removeBinding`. The write model is complete and unit-shaped.
47
+
48
+ ### Side C — the smoking gun: the "v2-style" sketch is already the command-bus
49
+
50
+ The "to be built when the endpoint lands" sketch in `ooze_redirect.rb` (L79-89) describes exactly what the **already-shipped** `force_compat.rb` does — and that file does **not** call any forces endpoint:
51
+
52
+ `C:\ruby_scripts\git\eco-helpers\lib\eco\api\usecases\graphql\compat\ooze_redirect\force_compat.rb`
53
+ - **L43-46, L62-66** — `process_ooze` → `save_force_changes!` → `graphql.page.execute_force_commands(id:, patch_ver:, commands:)`, i.e. it submits `force_col.pending_commands` **through `executeWorkflowCommands`**. The write half was *finished* the same day the TODO was written.
54
+
55
+ So the prose ("blocked on endpoint," "to be built") and the code ("here is the working write path through the command-bus") are in direct contradiction *within the same module*.
56
+
57
+ ---
58
+
59
+ ## 3. The read-vs-mutate conflation (the conceptual core)
60
+
61
+ Two genuinely distinct concerns were collapsed into one boolean:
62
+
63
+ | Concern | Mechanism | Status (as of 2026-06-30) |
64
+ |---|---|---|
65
+ | **(a) READ legacy forces on a page** | `Query::PageWithForces` — selects `forces { ...ForceFields }` on the page union | **WIP / broken**: per commit `de62d2d1`, the query "fails schema validation (PageUnion selections; `id` on DataFieldBinding/SectionBinding; unused ForceFields)." This is a *client query shape* defect. |
66
+ | **(b) MUTATE forces** | `executeWorkflowCommands(commands: [WorkflowCommandInput])` with `editForce`/`addBinding`/`removeForce`/… | **Working / shipped** since 2026-06-10 (`cd57b54`) and folded into the unified command-bus 2026-06-11 (`8e8f492`). |
67
+
68
+ The v2 mental model treats a force as a *page sub-object you read, then mutate in place*: `page.forces.get_by_name(...)`, `force.bindings.add(...)`, `force.custom_script = ...`, with a LISP `custom_script` you edit. In that model, reading and writing are the **same** object graph, so "I can't read forces" naturally implies "I can't do forces."
69
+
70
+ The new platform model is **imperative**: you don't read-then-mutate a live object; you *emit commands* (`editForce { id, customScript }`, `addBinding { forceId, ... }`) into `executeWorkflowCommands`, exactly like every other template/workflow change. In this model, **read and write are decoupled** — a write needs only the force/binding `id` and the new value; it does **not** require `PageWithForces` to be schema-valid.
71
+
72
+ **The conflation:** because the gate (`force_support?`) was attached to the *read* query, and because `force_compat.rb` *also* uses `PageWithForces` to fetch (`with_each_entry`, L30-40), the broken read poisoned the whole feature. The "blocked" conclusion was drawn from the broken READ query and then **over-generalised to ALL force operations** — including the writes that already worked. The v2 carryover (looking for a forces *endpoint*, i.e. a place to read forces from) is precisely what made the read query feel like the readiness signal.
73
+
74
+ ---
75
+
76
+ ## 4. Git timeline — what existed when
77
+
78
+ **Gem (`ecoportal-api-graphql`):**
79
+
80
+ | Date | Commit | Event |
81
+ |---|---|---|
82
+ | 2026-06-10 | `cd57b54` | `feat: Force model + fragment + mutation + query for ForceCompat support` — adds `Base::Force` (+ bindings, collection, `custom_script=` queues `editForce`), `Fragment::ForceFields`, **`Mutation::Page::ExecuteForceCommands` (write)** AND **`Query::PageWithForces` (read)** together. Writes were present from day one. |
83
+ | 2026-06-11 | `8e8f492` | `feat: expose PagesWorkflow space (W1-W5) — read models, command bus, fragments, specs, docs` — adds the full `WorkflowCommand` command-bus incl. `add_force/edit_force/remove_force/reorder_forces/add_binding/edit_binding/remove_binding`; `ExecuteForceCommands` becomes a deprecated delegator to `executeWorkflowCommands`. |
84
+ | 2026-06-16 | `bafbf80` | `fix: require all field_config byType modules in WorkflowCommand`. |
85
+
86
+ **eco-helpers (`ooze_redirect.rb` / `force_compat.rb`):**
87
+
88
+ | Date | Commit | Event |
89
+ |---|---|---|
90
+ | 2026-06-09 | `457eccd9` | `feat: GraphQL::Compat::OozeRedirect` — shim created. |
91
+ | 2026-06-09 | `244aff8a` | `docs: OozeRedirect Forces TODO block` — **the "blocked on GraphQL endpoint" prose written BEFORE the write path shipped.** At this moment the claim was *defensible*. |
92
+ | 2026-06-10 | `e3bc7f0a` | `feat: OozeRedirect::ForceCompat — force/binding support via GraphQL` — the working write path lands (`save_force_changes!` → `execute_force_commands`). **The TODO prose was now stale but was not removed.** |
93
+ | 2026-06-30 | `de62d2d1` | `fix(ooze-redirect): disable ForceCompat (forces WIP)` — `force_support?` was `defined?(Query::PageWithForces)`; once the gem force-loaded that became `true`, prepending `ForceCompat` for *every* case and routing `with_each_entry` through the broken read. Hard-disabled to `false`. **This correctly stopped the broken read from breaking toocs/cans — but the accompanying comment re-asserted the over-general "forces are NOT usable" claim, re-freezing the stale narrative instead of narrowing it to the read query.** |
94
+ | 2026-06-30 | `d12f5552` | `fix(ooze-redirect): faithful dry-run + slim shim` — current state. |
95
+
96
+ **Key finding:** the write command inputs existed (2026-06-10/11) *before* the "blocked" claim was last reinforced (2026-06-30). The reinforcement on 2026-06-30 had the gem's working force-write code sitting in the same dependency tree — yet the comment still said "blocked on GraphQL endpoint." The DECISIONS log entry of the **same day** (`[2026-06-30] Forces-dependent cases are HELD pending a Phase 0 live readiness test`, `ooze-graphql-native-migration/DECISIONS.md` L120-148) *did* finally record the correction ("the GraphQL force endpoint is NOT read-only or missing — the gem ships both … read AND … write"). So the correction landed in the **project decision log** but did **not** propagate back into the `ooze_redirect.rb` gate/comment that consumers and future agents actually read.
97
+
98
+ ---
99
+
100
+ ## 5. Root-cause chain (ranked)
101
+
102
+ 1. **Wrong readiness signal + read/write coupling (primary).** `force_support?` keyed on `defined?(Query::PageWithForces)` — a *read* class — as a proxy for "forces work." Because `force_compat.rb#with_each_entry` *fetches* via `PageWithForces`, the broken read could break unrelated cases, forcing a binary all-off switch. A capability that is two independent halves (read vs write) was gated by one boolean tied to the broken half. The write half was never given its own gate, so it died with the read.
103
+
104
+ 2. **Stale doc never re-examined after the feature shipped (close second).** The "blocked on endpoint" TODO was written 2026-06-09 (`244aff8a`) when it was *true*, then the write path shipped 2026-06-10 (`e3bc7f0a`) **in the same module**, and the TODO was never reconciled. Doc rot: the prose became a fossil that subsequent edits (incl. 2026-06-30) treated as ground truth and even *expanded* rather than questioned.
105
+
106
+ 3. **Verified platform fact lived only in machine-local memory, invisible across sessions/agents (structural).** The general command-bus architecture ("templates updated imperatively via ~80 `WorkflowCommandInput` types") was captured in `project_template_update_architecture.md` and `project_workflow_space.md` under `C:\Users\rella\.claude\projects\…\memory\` — a **per-machine, gitignored** store. That store is invisible to (a) other machines, (b) other agents/sessions, (c) anyone reading the repo. The fact was "known" in one silo but had no **code-adjacent, shared** form, so it could not contradict the stale comment at the point of use.
107
+
108
+ 4. **The captured general fact was never specialised to the force inference (reasoning gap).** Even where the command-bus was known, nobody made the *specific* deduction: "forces are members of the command-bus → forces are mutable on GraphQL → `force_support?` must not be hard-false for writes." The general → specific link was never drawn. Knowing "templates are updated via commands" did not automatically surface "therefore the forces-blocked claim is wrong," because forces were mentally filed under "legacy read object," not "command-bus member."
109
+
110
+ 5. **v2 endpoint mental model (contributing).** The team looked for a forces *endpoint* (a place to read/CRUD forces as objects) rather than recognising forces as just-another-command in an existing imperative bus. This is why the read query felt load-bearing and why the TODO sketch reinvented a v2-shaped `forces.get_by_name/.bindings.add` API that the command-bus had already obviated for writes.
111
+
112
+ ---
113
+
114
+ ## 6. Concrete corrections
115
+
116
+ ### (a) `ooze_redirect.rb` — gate + comment (DESCRIBE; not applied)
117
+
118
+ - **Split the capability into READ vs WRITE.** Do not let a single `force_support?` boolean conflate them.
119
+ - **Writes** (`editForce`/`addBinding`/`removeBinding`/`removeForce` via `executeWorkflowCommands`) are available **now** and do not depend on `PageWithForces`. The write path in `force_compat.rb#save_force_changes!` is correct as written.
120
+ - **Reads** (`Query::PageWithForces`) are the only WIP part. The blocker is a *client query-shape* bug (PageUnion selection set; `id` on `DataFieldBinding`/`SectionBinding`; unused `ForceFields`), not a missing backend endpoint.
121
+ - **Re-scope the gate.** Replace the hard `false` with a gate that reflects the real dependency: ForceCompat's *fetch* (`with_each_entry` via `PageWithForces`) is what must be gated, not force mutation per se. Options:
122
+ - Gate ForceCompat activation on a **read-readiness** check (only prepend `ForceCompat::Infrastructure` when `PageWithForces` is schema-valid against the live org), so toocs/cans never route through the broken read; **or**
123
+ - Decouple the fetch from the mutate: let ForceCompat reuse the *standard* page fetch and only add the write-command accumulation, fetching forces via a separate call (or a fixed `PageWithForces`) only for cases that actually touch forces. This lets writes proceed even while the read query is being fixed.
124
+ - **Rewrite the prose (L42-89, L113-122).** Delete "Force / binding operations are not available in GraphQL" and "blocked on GraphQL endpoint." Replace with the accurate split: "Force/binding **writes** are available now via `executeWorkflowCommands` (`editForce`/`addBinding`/`removeBinding`/`removeForce`). The only WIP is **reading** existing forces on a page (`Query::PageWithForces` fails schema validation — fix the selection set). The ~50% 'blocked' roster is **not** blocked on a missing endpoint; it is at most blocked on the read query for cases that must *inspect* current forces before mutating." Keep the case roster but relabel it "force-touching cases (verify read-vs-write need per case)."
125
+ - **Cross-link** the comment to the DECISIONS entry (`ooze-graphql-native-migration/DECISIONS.md` L120-148) so the gate and the decision log can't drift apart again.
126
+
127
+ ### (b) verified-platform-knowledge entry to capture
128
+
129
+ Create a durable, **repo-committed** platform-fact entry (not machine-local memory):
130
+
131
+ > **Forces on GraphQL are mutated via the `WorkflowCommandInput` command-bus, applied with the `executeWorkflowCommands` mutation.** Command keys: `addForce`, `editForce`, `removeForce`, `reorderForces`, `addBinding`, `editBinding`, `removeBinding` (gem: `Input::WorkflowCommand::*`, `COMMAND_MAP` L106-117). There is **no** separate forces write endpoint. A force `editForce` needs only `{ id, customScript }`; a binding needs `{ forceId, name, referenceId, type }`. Confirmed by Oscar (more than once) and by shipped gem code (`cd57b54` 2026-06-10, `8e8f492` 2026-06-11).
132
+ > **Only WIP part:** *reading* existing forces on a page via `Query::PageWithForces` — currently fails client-side schema validation (PageUnion selection set; `id` on `DataFieldBinding`/`SectionBinding`). This is a query-shape fix, not a backend gap.
133
+ > **Anti-pattern to retire:** the v2 "forces endpoint / `page.forces` live object" mental model. Forces are command-bus members, not a CRUD endpoint.
134
+
135
+ Suggested home: a committed `verified-platform-knowledge` doc (e.g. under `.ai-assistance/code/` or a `.ai-assistance/local/standards-requests/` entry that gets promoted to a shared, committed location — note: `.ai-assistance/local/standards-requests/` does **not currently exist** in this repo, see §7).
136
+
137
+ ### (c) process fix to prevent recurrence
138
+
139
+ 1. **Promote verified platform facts out of machine-local memory into committed, code-adjacent docs.** The command-bus fact lived only in `~/.claude/.../memory/` (gitignored, per-machine). A platform fact that gates real code MUST be captured where every agent/session/teammate and the code reviewer can see it — i.e. committed to the repo near the code it governs. This is the textbook case the `verified-platform-knowledge` standards request is meant to solve: a user-clarified fact that never became durable shared knowledge, so a stale "blocked" assertion survived and was reinforced.
140
+ 2. **Doc-and-gate co-location invariant.** When a feature ships its write path (commit `e3bc7f0a`), any "TODO/blocked" prose describing that feature in the *same module* must be reconciled in the same change. A lint/check or a PR-checklist item: "Did this commit make any nearby 'blocked/TODO/not available' comment stale?"
141
+ 3. **Never gate a multi-part capability on a single boolean tied to one part.** Capability gates should name the *specific* dependency (here: "ForceCompat fetch needs a schema-valid `PageWithForces`"), not a coarse "forces work." A gate comment should be falsifiable and cite the exact failing artifact.
142
+ 4. **Close the loop from DECISIONS log back to code.** The 2026-06-30 DECISIONS entry recorded the correct fact but the `ooze_redirect.rb` gate/comment was not updated to match. Decision-log entries that contradict a code comment should spawn a tracked task to fix the comment, or the decision isn't "done."
143
+
144
+ ---
145
+
146
+ ## 7. Note on the standards mechanism
147
+
148
+ The task referenced `.ai-assistance/local/standards-requests/` (a `verified-platform-knowledge` request). **That directory does not exist in this repo as of 2026-07-01** (`.ai-assistance/local/` was not present). This RCA is therefore a concrete, documented justification for creating that mechanism: a verified platform fact (forces = `Workflow*` command-bus), clarified by the maintainer more than once, was never captured as durable, shared, **committed** knowledge — so a stale "blocked on endpoint" assertion persisted across three weeks and was actively reinforced even after the contradicting code shipped in the same module. A committed verified-platform-knowledge store, co-located with the gated code and cross-linked from the DECISIONS log, would have surfaced the contradiction at the gate.