ecoportal-api-graphql 1.3.16 → 2.0.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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +384 -6
  3. data/lib/ecoportal/api/common/graphql/model/diffable/leaf_diff_service.rb +97 -0
  4. data/lib/ecoportal/api/common/graphql/model/diffable.rb +1 -0
  5. data/lib/ecoportal/api/graphql/base/action.rb +1 -1
  6. data/lib/ecoportal/api/graphql/base/action_event.rb +23 -0
  7. data/lib/ecoportal/api/graphql/base/action_event_page.rb +16 -0
  8. data/lib/ecoportal/api/graphql/base/activity.rb +38 -0
  9. data/lib/ecoportal/api/graphql/base/activity_log.rb +38 -0
  10. data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +12 -7
  11. data/lib/ecoportal/api/graphql/base/auth_log.rb +24 -0
  12. data/lib/ecoportal/api/graphql/base/authorization_result.rb +20 -0
  13. data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +17 -5
  14. data/lib/ecoportal/api/graphql/base/file_container.rb +12 -4
  15. data/lib/ecoportal/api/graphql/base/location_classification_type.rb +2 -0
  16. data/lib/ecoportal/api/graphql/base/location_structure.rb +3 -0
  17. data/lib/ecoportal/api/graphql/base/minimal_dashboard.rb +25 -0
  18. data/lib/ecoportal/api/graphql/base/organization.rb +2 -0
  19. data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +26 -3
  20. data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +47 -8
  21. data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +5 -2
  22. data/lib/ecoportal/api/graphql/base/page/data_field/people.rb +50 -12
  23. data/lib/ecoportal/api/graphql/base/page/data_field.rb +12 -18
  24. data/lib/ecoportal/api/graphql/base/permissions.rb +15 -0
  25. data/lib/ecoportal/api/graphql/base/person_member_changelog.rb +23 -0
  26. data/lib/ecoportal/api/graphql/base/preset_view.rb +136 -1
  27. data/lib/ecoportal/api/graphql/base/register.rb +5 -0
  28. data/lib/ecoportal/api/graphql/base.rb +9 -0
  29. data/lib/ecoportal/api/graphql/builder/contractor_entity.rb +10 -0
  30. data/lib/ecoportal/api/graphql/builder/page.rb +0 -16
  31. data/lib/ecoportal/api/graphql/builder/template.rb +18 -0
  32. data/lib/ecoportal/api/graphql/compat/pages.rb +7 -2
  33. data/lib/ecoportal/api/graphql/compat/response.rb +14 -0
  34. data/lib/ecoportal/api/graphql/concerns/threadable.rb +58 -0
  35. data/lib/ecoportal/api/graphql/concerns.rb +1 -0
  36. data/lib/ecoportal/api/graphql/connection/action.rb +1 -1
  37. data/lib/ecoportal/api/graphql/connection/action_category.rb +1 -1
  38. data/lib/ecoportal/api/graphql/connection/activity.rb +11 -0
  39. data/lib/ecoportal/api/graphql/connection/activity_log.rb +11 -0
  40. data/lib/ecoportal/api/graphql/connection/auth_log.rb +11 -0
  41. data/lib/ecoportal/api/graphql/connection/contractor_entity.rb +1 -1
  42. data/lib/ecoportal/api/graphql/connection/minimal_dashboard.rb +11 -0
  43. data/lib/ecoportal/api/graphql/connection/page.rb +1 -1
  44. data/lib/ecoportal/api/graphql/connection/pages_workflow_command.rb +1 -1
  45. data/lib/ecoportal/api/graphql/connection/person_member.rb +1 -1
  46. data/lib/ecoportal/api/graphql/connection/person_member_changelog.rb +11 -0
  47. data/lib/ecoportal/api/graphql/connection/preview_page.rb +1 -1
  48. data/lib/ecoportal/api/graphql/connection.rb +5 -0
  49. data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +21 -1
  50. data/lib/ecoportal/api/graphql/file_upload/client.rb +199 -140
  51. data/lib/ecoportal/api/graphql/fragment/activity.rb +84 -0
  52. data/lib/ecoportal/api/graphql/fragment/activity_log.rb +75 -0
  53. data/lib/ecoportal/api/graphql/fragment/auth_log.rb +29 -0
  54. data/lib/ecoportal/api/graphql/fragment/authorization_result.rb +14 -0
  55. data/lib/ecoportal/api/graphql/fragment/location_node.rb +1 -0
  56. data/lib/ecoportal/api/graphql/fragment/minimal_dashboard.rb +31 -0
  57. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +21 -0
  58. data/lib/ecoportal/api/graphql/fragment/permissions.rb +98 -0
  59. data/lib/ecoportal/api/graphql/fragment/person_member_changelog.rb +33 -0
  60. data/lib/ecoportal/api/graphql/fragment.rb +7 -0
  61. data/lib/ecoportal/api/graphql/input/action/update.rb +12 -0
  62. data/lib/ecoportal/api/graphql/input/contractor_entity/manager_settings.rb +20 -0
  63. data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +6 -27
  64. data/lib/ecoportal/api/graphql/input/contractor_entity.rb +1 -0
  65. data/lib/ecoportal/api/graphql/input/date_range.rb +16 -0
  66. data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +2 -2
  67. data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +2 -2
  68. data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +6 -2
  69. data/lib/ecoportal/api/graphql/input/preset_view/update.rb +16 -1
  70. data/lib/ecoportal/api/graphql/input/workflow_command/add_checklist_field_item.rb +19 -0
  71. data/lib/ecoportal/api/graphql/input/workflow_command/add_smart_fill_config.rb +23 -0
  72. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage.rb +3 -2
  73. data/lib/ecoportal/api/graphql/input/workflow_command/add_viewable_field.rb +45 -0
  74. data/lib/ecoportal/api/graphql/input/workflow_command/archive_public_config.rb +19 -0
  75. data/lib/ecoportal/api/graphql/input/workflow_command/create_public_config.rb +19 -0
  76. data/lib/ecoportal/api/graphql/input/workflow_command/edit_checklist_field_item.rb +19 -0
  77. data/lib/ecoportal/api/graphql/input/workflow_command/edit_field_configuration.rb +12 -2
  78. data/lib/ecoportal/api/graphql/input/workflow_command/edit_public_config.rb +19 -0
  79. data/lib/ecoportal/api/graphql/input/workflow_command/edit_smart_fill_config.rb +21 -0
  80. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_advanced_settings.rb +19 -0
  81. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_basic_settings.rb +27 -0
  82. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_container_uid.rb +45 -0
  83. data/lib/ecoportal/api/graphql/input/workflow_command/{edit_page_creator_permissions.rb → edit_template_creator_permissions.rb} +3 -2
  84. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_descriptions.rb +19 -0
  85. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/action_list.rb +31 -0
  86. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/cross_reference.rb +3 -1
  87. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/date.rb +15 -2
  88. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/file.rb +21 -0
  89. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/image_gallery.rb +3 -2
  90. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/people.rb +27 -3
  91. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/plain_text.rb +3 -2
  92. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/rich_text.rb +3 -2
  93. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/select.rb +3 -2
  94. data/lib/ecoportal/api/graphql/input/workflow_command/move_field.rb +3 -2
  95. data/lib/ecoportal/api/graphql/input/workflow_command/move_viewable_field.rb +33 -0
  96. data/lib/ecoportal/api/graphql/input/workflow_command/remove_checklist_field_item.rb +19 -0
  97. data/lib/ecoportal/api/graphql/input/workflow_command/remove_smart_fill_config.rb +19 -0
  98. data/lib/ecoportal/api/graphql/input/workflow_command/remove_viewable_field.rb +21 -0
  99. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_forces.rb +3 -2
  100. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_section.rb +3 -2
  101. data/lib/ecoportal/api/graphql/input/workflow_command.rb +43 -5
  102. data/lib/ecoportal/api/graphql/input.rb +1 -0
  103. data/lib/ecoportal/api/graphql/interface/base_page.rb +36 -3
  104. data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +2 -1
  105. data/lib/ecoportal/api/graphql/logic/connection.rb +17 -0
  106. data/lib/ecoportal/api/graphql/logic/input.rb +127 -0
  107. data/lib/ecoportal/api/graphql/model/action_event.rb +14 -0
  108. data/lib/ecoportal/api/graphql/model/action_event_page.rb +10 -0
  109. data/lib/ecoportal/api/graphql/model/activity.rb +14 -0
  110. data/lib/ecoportal/api/graphql/model/activity_log.rb +31 -0
  111. data/lib/ecoportal/api/graphql/model/auth_log.rb +11 -0
  112. data/lib/ecoportal/api/graphql/model/authorization_result.rb +12 -0
  113. data/lib/ecoportal/api/graphql/model/minimal_dashboard.rb +14 -0
  114. data/lib/ecoportal/api/graphql/model/organization.rb +27 -0
  115. data/lib/ecoportal/api/graphql/model/permissions.rb +179 -0
  116. data/lib/ecoportal/api/graphql/model/person_member.rb +7 -0
  117. data/lib/ecoportal/api/graphql/model/person_member_changelog.rb +15 -0
  118. data/lib/ecoportal/api/graphql/model.rb +9 -0
  119. data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +20 -10
  120. data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +25 -10
  121. data/lib/ecoportal/api/graphql/mutation/contractor_entity/update_manager_settings.rb +37 -0
  122. data/lib/ecoportal/api/graphql/mutation/contractor_entity.rb +1 -0
  123. data/lib/ecoportal/api/graphql/mutation/file_container/upload.rb +78 -0
  124. data/lib/ecoportal/api/graphql/mutation/file_container.rb +15 -0
  125. data/lib/ecoportal/api/graphql/mutation/template/update_related_page.rb +79 -0
  126. data/lib/ecoportal/api/graphql/mutation/template.rb +1 -0
  127. data/lib/ecoportal/api/graphql/mutation.rb +1 -1
  128. data/lib/ecoportal/api/graphql/payload/contractor_entity/manager_settings.rb +20 -0
  129. data/lib/ecoportal/api/graphql/payload/contractor_entity.rb +1 -0
  130. data/lib/ecoportal/api/graphql/payload/template/update_related_page.rb +15 -0
  131. data/lib/ecoportal/api/graphql/payload/template.rb +1 -0
  132. data/lib/ecoportal/api/graphql/payload/upload.rb +12 -0
  133. data/lib/ecoportal/api/graphql/payload.rb +1 -0
  134. data/lib/ecoportal/api/graphql/query/activity_logs.rb +78 -0
  135. data/lib/ecoportal/api/graphql/query/auth_logs.rb +51 -0
  136. data/lib/ecoportal/api/graphql/query/dashboard_search.rb +57 -0
  137. data/lib/ecoportal/api/graphql/query/location_structure.rb +2 -0
  138. data/lib/ecoportal/api/graphql/query/location_structures.rb +2 -0
  139. data/lib/ecoportal/api/graphql/query/page.rb +6 -1
  140. data/lib/ecoportal/api/graphql/query/page_activities.rb +56 -0
  141. data/lib/ecoportal/api/graphql/query/permissions.rb +35 -0
  142. data/lib/ecoportal/api/graphql/query/person_member_changelogs.rb +53 -0
  143. data/lib/ecoportal/api/graphql/query.rb +6 -0
  144. data/lib/ecoportal/api/graphql_version.rb +1 -1
  145. metadata +73 -12
  146. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page.rb +0 -28
  147. data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +0 -36
  148. data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +0 -40
  149. data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eada698a590c0214f8eb7a37255efe68c2573de6c73e3e1ce42ba6a8ff5accb
4
- data.tar.gz: 316b65e4027224711711d2248ada651911b1e64ce48aeff6bffb22144e53cd11
3
+ metadata.gz: e679d934d9305d1926abe7251617eea0e5333c04665672b25f5ad38afb67d9b3
4
+ data.tar.gz: 2b8047d56b9af371b71308bcabce23f8e4c4b67d014ce7a4095386fadb1f89f4
5
5
  SHA512:
6
- metadata.gz: f0894ce98e42628e50bb594ccbaa0050ae07518517a08745d54f5f61dc4031f959163b69caa1768ed978d0499abbb478c2dc4811867239373c324cac8008cc2a
7
- data.tar.gz: 43e3dcf9dd61f521573a2249d26c92a571c23f06f2c742e9b19aa5554832b1195b251f0832da70f6231c68c0f94e108ff6487738a1e81e1c8c7d214944a98e0d
6
+ metadata.gz: 6ec6f2792de809534943497a98a74ec012b061778d379bb9f8ac205805cd9a629386c3293ee00e647549564fed5edbbc80e73cf856340922548309b8e9c68435
7
+ data.tar.gz: 948f0064aa886e310fc24f78a3919cbccc3b192495c724d86d003e1ff03bd39d2d12b312e82c04ca6626779abd822f9f9914de44b651c4903ba51d275085f7e8
data/CHANGELOG.md CHANGED
@@ -2,15 +2,393 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [1.3.16] - 2026-08-13
5
+ ## [Unreleased]
6
+
7
+ ## [2.0.0] - 2026-08-13
8
+
9
+ Major release: everything accumulated on `main` since `v1.3.14` — the 2026-07-13/07-25/07-31
10
+ schema syncs, command-bus completion (105/105 commands), native file upload, and a long run of
11
+ correctness fixes. **Breaking, but narrowly:** every removed or reshaped surface either targeted
12
+ a mutation the live schema never had (so it could never have worked) or was a dead accessor with
13
+ no consumer — a 2026-07-30 sweep of `eco-helpers`, `integration-training`, `multi_org_api` and
14
+ `multi-org-idle` found no consumer of any breaking surface (re-confirmed 2026-08-12). The major
15
+ bump is deliberate: consumer repos declare `~> 1.3` (which allows 1.4.x), so a minor would have
16
+ been adopted silently on the next `bundle update`; 2.0.0 makes adoption a per-repo decision.
17
+
18
+ ★ **What starts WORKING when you adopt this** (silently-broken → live, coming from ≤ 1.3.14):
19
+ the page-level `executeWorkflowCommands` bus, File-field attach writes, `updatePresetView`
20
+ column edits, AI-summary mutations, viewable-field management, Date-field renewals, and
21
+ eco-helpers' `OozeRedirect` force path (always errored before — re-verify on a sandbox org
22
+ before trusting it in a customer run). Review those paths before assuming a no-op upgrade.
23
+
24
+ Note: this release does not descend from `v1.3.15`, which shipped from the `release/1.3.15`
25
+ branch forked at 1.3.14 — do not expect a linear tag history.
26
+
27
+ ★ **The published gem is now `lib/` only.** `spec.files` excluded just `test|spec|features`, so
28
+ every top-level directory added since shipped by default — 244 files of internal `.ai-assistance/`
29
+ notes, `docs/` (including the rolling worklog), and `.claude/`. `spec.files` is now an allowlist
30
+ (`lib/`, `exe/`, README, LICENSE, CHANGELOG), which also drops the package from 959 KB to 195 KB.
31
+ All 568 `lib/` files are unchanged; nothing outside `lib/` was ever loaded at runtime.
32
+
33
+ ### Added
34
+
35
+ - `WorkflowCommandInput` commands: `editTemplateBasicSettings`, `editTemplateAdvancedSettings`,
36
+ `editTemplateCreatorPermissions`, `editTemplateDescriptions`, `createPublicConfig`,
37
+ `editPublicConfig`, `archivePublicConfig`, `addChecklistFieldItem`, `editChecklistFieldItem`,
38
+ `removeChecklistFieldItem`, `addSmartFillConfig`, `editSmartFillConfig`, `removeSmartFillConfig`.
39
+ - `addStage` gained `tags` (create a stage already tagged).
40
+ - `editFieldConfiguration` gained `addActionIds`/`removeActionIds`; `crossReference` gained
41
+ `addReferenceIds`/`removeReferenceIds`; `imageGallery` gained `addImageSourceIds`/`removeImageIds`;
42
+ new `file` by-type field config (`addFileContainerIds`/`removeItemIds`).
43
+ - **The WorkflowCommandInput command bus is now at full coverage: 105/105 commands, 14/14 field-config
44
+ builders, zero dead keys, zero unreachable keys** — verified against the live `20260731` schema.
45
+ Granular template management no longer has holes.
46
+ - **`tests/validate_commands.rb`** — new offline gate, the command-bus sibling of
47
+ `validate_queries.rb`. That gate can only see the documents we *send*; every command travels as a
48
+ member of a `[WorkflowCommandInput!]` **variable**, so unwired commands and stale key allow-lists
49
+ were completely invisible to it. The new script reports unwired commands, dead keys (gem sends,
50
+ schema rejects), orphaned `COMMAND_MAP` entries, missing keys, and byType builder coverage.
51
+ Fails the build on the first three. Wire it into CI next to `validate-queries`.
52
+ - **`addViewableField` / `moveViewableField` / `removeViewableField` commands** — these replaced
53
+ `WorkflowEditFieldConfigurationPeopleInput.addViewableFields`/`removeViewableFields`, which the
54
+ 20260725 drift REMOVED. Between that drift and now, viewable-field management was impossible
55
+ through the gem: the old keys were dead and the new commands were unwired. The replacement also
56
+ adds ordering (`anchorId` + `anchorPosition`) the flat arrays could not express.
57
+ - **`Input::WorkflowCommand::EditFieldConfiguration::ActionList`** — the 14th and last unwired
58
+ `byType` member, so ActionsList field configuration is reachable at last.
59
+ - **`updateRelatedPage` mutation** + payload + `Builder::Template#update_related_page`. The worklog
60
+ carried this as an *ecoportal backend* gap ("the only genuine structural template omission") — that
61
+ was stale, the mutation is in the live schema, so the gap was ours. Related-pages entries could be
62
+ created and destroyed but never edited, forcing destroy+recreate for a title change, which
63
+ discards the entry id.
64
+ - **Cross-version template identity is now readable.** `CommonPageUnion` selects
65
+ `template { templateContainerUid active }`, and `Interface::BasePage` exposes `#template`
66
+ (a read-only `Model::Template::Information`) plus the `#templateContainerUid` shortcut.
67
+ `templateContainerUid` is the template **series** key: many versions may carry the same value,
68
+ but only one may be published (`active: true`) per organization at a time — publishing another
69
+ with the same uid unpublishes the incumbent. Previously the value was fetchable from the API but
70
+ unreachable through the gem (the embed was commented out and no fragment selected it).
71
+ Semantics documented in `.ai-assistance/code/ecoPortal_architecture/02_data_model.md`.
72
+ - **`editTemplateContainerUid` workflow command** — `Input::WorkflowCommand::EditTemplateContainerUid`,
73
+ registered on the command bus. Unlike its `editTemplate*` siblings it raises on a nil value rather
74
+ than emitting an empty command (the schema argument is `String!`) and validates the backend's
75
+ `3-20 chars, [a-zA-Z0-9-_.]` format up front.
76
+ - **DataField `description` is now readable.** Added to the `dataFieldInterface` fragment and
77
+ declared `read_only: true` on `Base::Page::DataField`. It is the hidden per-field note that
78
+ integrations reuse as a pseudo external-id on fields. Read-only is deliberate: `description`
79
+ exists on `DataFieldsInterface` but on no member of `DataFieldInput`, so a writable accessor would
80
+ dirty the field and push a key `updatePage` cannot accept — the only writer is the
81
+ `editFieldConfiguration` workflow command, which is confirmed to work on **page instances** as
82
+ well as templates.
83
+ - `tests/page_field_description_update.rb` — scripted read-modify-verify for a single dataField
84
+ `description` on a page instance, with a `--dry-run` mode and a `canRestructure`/`canConfigure`
85
+ pre-flight.
86
+ - **`Diff::Pairing::Signals.genome` now recognises lineage ancestry** (scores `0.9`, just below
87
+ an exact match) when one signature is a strict prefix of the other. Verified live 2026-08-05
88
+ on a same-org restore of a full template backup: a restore re-mints every Mongo id and
89
+ APPENDS one segment to every membrane AND force genome signature (28/28 + 4/4 followed the
90
+ rule, e.g. `ktnrya` → `ktnrya877l`) — equality-only scoring handed the exact cross-object
91
+ case the pairing engine exists for (UAT backup → PROD restore) a `0.0` mismatch. Signatures
92
+ shorter than 4 chars are rejected as ancestry evidence (chance-collision guard).
93
+ - **New manual tool `tests/template_backup_diff.rb`**: loads the platform's template BACKUP
94
+ files (YAML = Mongoid/BSON object dump — loaded via shims, no bson dependency; JSON = same
95
+ document, ids as hex/times as ISO8601), converts YAML→JSON, and produces an id-keyed deep
96
+ diff (adds/removes/changes/reorders, bookkeeping noise hidden by default, nil≡empty-container
97
+ lenience for document-age drift). Detects a foreign id-space and points at
98
+ `Diff::CrossObjectDiff` instead of emitting a misleading add/remove-everything report.
99
+ - Scoping project `.ai-assistance/projects/data-toolset/` — coordination of the data
100
+ import/extract/migration tool-set (legacy imports, template and register metadata table
101
+ definitions, BI warehouse extracts, spreadsheet and back-end imports, register-to-register and
102
+ org-to-org migrations, register-revamp mapping and readiness reporting). Capability map C1-C8,
103
+ verified inventory of existing tooling, the two shared primitives the set is missing (field
104
+ identity across template versions, and a versioned table projection), staged plan through to
105
+ promotion into its own repo, and the open blockers.
106
+
107
+ ### Added (read-model / type layer — additive, non-breaking)
108
+
109
+ - **System access logs:** `Base::AuthLog` / `Model::AuthLog` (+ `Connection::AuthLog`, `:AuthLog`
110
+ fragment) and `Model::Organization#authLogs` (`Query::AuthLogs`).
111
+ - **Changelog history:** `Base::PersonMemberChangelog` / `Model::PersonMemberChangelog`
112
+ (+ `Connection::PersonMemberChangelog`, `:PersonMemberChangelog` fragment),
113
+ `Model::Organization#personMemberChangelogs` (`Query::PersonMemberChangelogs`), and
114
+ `Model::PersonMember#changelogs`.
115
+ - **Dashboards:** `Base::MinimalDashboard` / `Model::MinimalDashboard`
116
+ (+ `Connection::MinimalDashboard`, `:MinimalDashboard` fragment) and
117
+ `Model::Organization#dashboardSearch` (`Query::DashboardSearch`).
118
+ - **Actions:** `Base::ActionEvent` / `Model::ActionEvent` and `Base::ActionEventPage` /
119
+ `Model::ActionEventPage`.
120
+ - **RS/location:** read-only `LocationStructure#generating`/`#aiPrompt`,
121
+ `LocationClassificationType#aiGenerated` (selected in the structure queries + `:LocationNode`
122
+ fragment).
123
+ - **Contractor:** `updateContractorManagerSettings` mutation
124
+ (`Mutation::ContractorEntity::UpdateManagerSettings` + `Input`/`Payload::…::ManagerSettings`,
125
+ exposed as `Builder::ContractorEntity#update_manager_settings`) and read-only
126
+ `Organization#allowContractorAdminsToAddAdmins`.
127
+ - **`Base::AuthorizationResult` / `Model::AuthorizationResult`** (+ `:AuthorizationResult` fragment) —
128
+ the reusable node type behind the schema's many `canXXX: AuthorizationResult` permission fields;
129
+ wired `Model::PersonMember#canManageContractorAdmins`. Unblocks the deferred `Permissions`-parented
130
+ `canXXX` fields (which still await a `Permissions` model).
131
+ - **`Base::Permissions` / `Model::Permissions`** (+ `:Permissions` fragment) — the org-level ability
132
+ set (`Organization.permissions`). Models all **83** `canXXX: AuthorizationResult!` fields as
133
+ `embeds_one` (e.g. `dashboardCanManage`, `locationStructuresCanAiGenerateStructure`,
134
+ `generatedPageFilterCanUseSmartQuery`). Wired `Model::Organization#permissions` (typed embed) plus a
135
+ native `Query::Permissions` (`currentOrganization.permissions`). The `:Permissions` fragment spreads
136
+ `:AuthorizationResult` per field (recursively assembled).
137
+
138
+ ### Added (polymorphic activity feeds — the REAL access-logs & page-history fields)
139
+
140
+ The web "System Access Logs" page and page "history changelog" are driven by
141
+ `currentOrganization.activityLogs` (a UNION) and `page.activities` (an INTERFACE) — NOT the flat
142
+ `authLogs`/`personMemberChangelogs` types. These add the polymorphic read models for the real fields.
143
+
144
+ - **System Access Logs:** `Base::ActivityLog` / `Model::ActivityLog` (+ nested `Model::ActivityLog::Resource`,
145
+ `Connection::ActivityLog`, `:ActivityLog` fragment) over the `ActivityLogUnion` (9 members:
146
+ DashboardAccess/Create/Delete/Update, DeniedDashboardAccess, DeniedPageAccess, FileDownload, PageAccess,
147
+ ScimIntegrationAccess). Models the fields common to all members + `__typename` passthrough; the Dashboard*
148
+ members' `parentResource*` + `resource { isVisible: AuthorizationResult }` are nullable embeds. The
149
+ fragment uses inline fragments per member (spreading `:ActivityLogCommon` on `BaseActivityLogInterface`
150
+ and `:ActivityLogResource` on `BaseDashboardActivityLogInterface`). Exposed as
151
+ `Model::Organization#activityLogs` (`Query::ActivityLogs`) with the full filter set
152
+ (`userIds`, `resourceIds`, `filterSuperusers`, `activityType`, `resourceType: ActivityLogTypesEnum`,
153
+ `source: SourceEnum`, `dateFilter: DateRange`, cursor pagination). New `Input::DateRange`.
154
+ - **Page history / activities:** `Base::Activity` / `Model::Activity` (+ `Connection::Activity`,
155
+ `:Activity` fragment) capturing the `ActivityInterface` common fields (id, `__typename`, priority,
156
+ system, fromPublicTemplate, stageId, categories/subkey, source/user scalars, createdAt/updatedAt,
157
+ personMember, user) across its ~61 concrete types — an unmodelled type still resolves as a plain
158
+ `Model::Activity`. Exposed as `Model::Organization#pageActivities` (`Query::PageActivities`) resolving
159
+ `page(id:).activities(searchConf:, …)`; because `page` returns a `PageUnion`, the connection is selected
160
+ via inline fragments on BasicPage/PhasedPage (`:PageActivities` + `:ActivityConnection` fragments).
161
+
162
+ ### Changed
163
+
164
+ - `ecoportal-api` dependency floor raised to `0.10.17`: inherits the `WithRetry` fix that
165
+ makes `HTTP::TimeoutError` (connect/read/write timeouts) retryable — previously a single
166
+ transient timeout killed whole batch loops (live-confirmed on Farmers, 2026-07-30).
167
+ - **`graphlient` dependency now points at the published `0.9.0` release** instead of the fork's
168
+ frozen git branch (`rellampec/graphlient@pre-v0.0.9`). `0.9.0` ships the same DSL/directive
169
+ work the gem already relied on (`#115` `to_query_string`/`spread`, `#116` directives DSL,
170
+ `spread(on:)` inline fragments, `fragment(:Name, on:)`, custom scalar registration) — non-breaking,
171
+ since the gem's own usage of these APIs is unchanged. `Gemfile` git override removed;
172
+ `ecoportal-api-graphql.gemspec` bound updated `>= 0.8.0, < 0.9` → `>= 0.9.0, < 0.10`.
173
+
174
+ ### Changed (BREAKING)
175
+
176
+ - `reorderForces` and `reorderSection`: `after`/`before` replaced by `anchorId`/`anchorPosition`.
177
+ - `moveField`: gained `anchorId`/`anchorPosition`.
178
+ - SmartFill removed from `date`/`plainText`/`richText`/`select` field configs — moved to the
179
+ dedicated `*SmartFillConfig` commands above.
180
+ - **AI summary mutations rewired to the fields that actually exist.** Both targeted mutation names
181
+ that are absent from every introspection we hold (2026-07-13 and 2026-07-30), so neither ever
182
+ worked:
183
+ - `Mutation::AiSummary::Generate`: `generateAiSummary(pageId:, stageId:)` →
184
+ **`generatePageSummary(input: GenerateInput!)`**. `#query` now takes `page_id:` / `stage_id:`
185
+ (was `pageId:` / `stageId:`) and wraps them into `input`.
186
+ - `Mutation::AiSummary::SubmitFeedback`: `submitAiSummaryFeedback(aiSummaryVersionId:, sentiment:)`
187
+ → **`submitPageAiSummaryFeedback(input: SubmitFeedbackInput!)`**. `#query` takes
188
+ `ai_summary_version_id:` / `sentiment:`. The enum is `SentimentEnum`, not
189
+ `AiSummarySentimentEnum`, and its values are **`positive` / `negative`** — the class previously
190
+ documented "THUMBS_UP"/"THUMBS_DOWN", which the schema has never accepted. `#query` validates
191
+ the value up front.
192
+ - `Base::AiSummaryVersion` dropped `prompt`, `inputTokens`, `outputTokens`, `totalTokens`,
193
+ `resourceType`, `ratings` — none exist on the type; they always read nil. Real fields:
194
+ `aiSummary`, `alreadyRated`, `createdAt`, `dataFieldLogs`, `generationErrors`, `id`,
195
+ `pagePatchVer`, `status` (the last three complex ones stay unmodelled until verified).
196
+
197
+ _(The `Payload::OkPayload` errors-modelling fix from the same commit is NOT breaking and
198
+ already shipped in 1.3.15 — see that release's section.)_
199
+ - **`FileUpload::Client` reimplemented natively from a captured web upload** (breaking, but the
200
+ old path could not work). The real flow — verified from a HAR of the React frontend on
201
+ 2026-07-30, then cross-checked against the live schema — is three steps, all GraphQL except
202
+ the storage POST:
203
+ 1. `FileSignature` (GraphQL) → presigned policy + credentials
204
+ 2. multipart POST to S3 → `204`; fields in order: `key`, `AWSAccessKeyId`, `policy`,
205
+ `signature`, `content-type`, `x-amz-server-side-encryption`, `file`
206
+ 3. **`uploadFile` (GraphQL)** → the `FileContainer`, whose `id` page mutations consume
207
+
208
+ The previous implementation was a port of `ecoportal-api-v2`'s REST S3 code and did
209
+ `POST /api/v2/<org>/s3/files` + polling. **The platform does not use those endpoints for this
210
+ flow** — there is no register-over-REST and no poll — so the 1.3.15 change that made those
211
+ calls authenticate (`X-ECOPORTAL-API-KEY`) fixed a step that does not exist. No API key is
212
+ needed for file upload at all; `api_key:` on `GraphQL.new` remains available but unused here.
213
+ - **New:** `Mutation::FileContainer::Upload` (`uploadFile`), `Payload::Upload`, and
214
+ `Concerns::Threadable` (bounded concurrency, ported from `ecoportal-api-v2` rather than
215
+ depended on, since that gem is being decommissioned).
216
+ - **New: `#upload_all(paths, threads: 4) { |result| }`** — concurrent multi-file upload with
217
+ per-file error isolation (a single failure never raises out of the batch; each `Result`
218
+ carries its own `error`/`container_id`), presigning **once** per batch, and a `Result` struct
219
+ shaped like v2's `S3::Files::BatchUpload::FileResult`. `threads: 1` runs inline.
220
+ - **New: stage hooks** — `client.on(:signature|:storage|:register) { … }`, chainable, for
221
+ logging/instrumentation/middleware around each step.
222
+ - **Fixed: storage keys no longer collide.** The key is
223
+ `uploads/<userId>/<epoch-ms>-<token>-/<basename>` as the web client builds it; the old
224
+ `upload_prefix + filename` concatenation meant two uploads of `report.pdf` overwrote each
225
+ other in the bucket.
226
+ - **Fixed: `x-amz-server-side-encryption` is read from the policy** (base64 JSON `conditions`)
227
+ instead of being hardcoded to `AES256` — the value now comes from the same source S3 validates
228
+ against, with `AES256` only as a fallback.
229
+ - **Fixed: `Base::FileContainer`** declared `activeVersionId`, which does not exist on the type
230
+ (it is `activeVersion`) and always read nil; `otherTags` was missing, which matters because
231
+ `UploadInput.otherTags` is the only way to tag a container — now exposed as
232
+ `upload(path, tags: […])`.
6
233
 
7
234
  ### Fixed
8
235
 
9
- - Packaging-only republish of `1.3.15` with an allowlisted gemspec (backported from `2.0.0`).
10
- Versions `1.3.9`-`1.3.15` shipped internal repository content (`.ai-assistance/` project notes,
11
- `docs/worklog.md`, architecture docs) to rubygems.org via the old denylist `spec.files`; they
12
- will be yanked. `1.3.16` is the identical `lib/` code (including the `1.3.15` OozeRedirect
13
- `executeWorkflowCommands` fix) packaged clean, so the `1.x` line keeps a clean latest release.
236
+ - **Cascaded leaf arrays are no longer dropped from `as_update`.** Leaf arrays declared with
237
+ `passarray`/embeds-many (id-lists, tag-lists, config-lists) were invisible to the diff: the
238
+ default `DiffService` strips cascaded keys from the flat classic diff, and `diff_reduce` only
239
+ re-adds children that are a `GraphQL::Model` an `ArrayModel`/`CollectionModel` is not so
240
+ the array change was silently discarded and no mutation was issued for it (SYNTHESIS.md 3.1).
241
+ The `DataField`-local `LeafDiffService` is promoted to the common layer
242
+ (`common/graphql/model/diffable/leaf_diff_service.rb`): it keeps cascaded doc keys in the flat
243
+ diff, preserves the flat-diff placeholder for non-model leaf children while still cascading
244
+ into real nested models and dropping `root!` children, and treats a leaf array as an opaque
245
+ full-set replace (no id-keyed nested patch-ops), matching how the write inputs consume these
246
+ arrays. Set as `DIFF_CLASS` on `Interface::BasePage` (covers Basic + Phased), `Base::Register`
247
+ and `Base::PresetView`; `Base::Page::DataField` now references the promoted class with
248
+ identical behaviour. Newly detected: page `otherTags` (full-set replace) and `locations`
249
+ (`[{id:}]` via `location_ids=`), `Register#filterTags`, and `PresetView#fieldConfigurations`.
250
+ - `Base::PresetView` now emits a schema-valid `updatePresetView` input for its column
251
+ configuration. The read model exposes the collection as `fieldConfigurations` (the key the
252
+ API returns), but `UpdatePresetViewInput` calls it `fieldConfigs` **and** takes it as a
253
+ `FieldConfigurationOneToManyInput` — `{ additions:, deletions:, updates: }` — not as the
254
+ read list; both facts are schema-confirmed in the live introspection dump (20260730), where
255
+ the field is also NON_NULL with no default, i.e. required on every call. Emitting the read
256
+ key would be an unknown-argument error and emitting the read list under the write key would
257
+ be a type error, either of which rejects the WHOLE mutation, a `name`-only edit included.
258
+ `#as_input` (and `Input::PresetView::Update.from_model`, which shares the same reshape, so
259
+ the implicit and explicit converter seams agree) now renames the key, computes the
260
+ additions/deletions/updates delta directly from `doc` vs `original_doc` — matching configs on
261
+ their `id` and whitelisting the fields `FieldConfigurationInput` accepts, so the read-only
262
+ `__typename` is dropped — and always sets `fieldConfigs`, using the no-op `{}` when the
263
+ columns did not change. A columns-only change is now sent at all: it was previously dropped
264
+ from the flat diff, so `as_input` returned `nil` and no mutation was issued.
265
+ `Input::PresetView::Update` declared `fieldConfigs` as a `passarray`; it is now a
266
+ `passthrough`, matching the object the schema expects. The create path is unaffected —
267
+ `CreatePresetViewInput.fieldConfigs` is a plain `[FieldConfigurationInput!]!` and
268
+ `Input::PresetView::Create` already used the write key with the right shape.
269
+ - `Base::Page::DataField::FileField` now reads and writes the shape the live API actually
270
+ uses. WRITE: `as_input` emits `file: { id:, items: [ItemInput] }` — live `FileInput` has
271
+ no `fileContainerIds` member, so every File-field write was rejected (live-confirmed
272
+ 2026-08-01 via `pages/file_upload`; the 4-step upload passed, the attach failed). The
273
+ backend applies `items` as full-replace (any existing item id not echoed back is
274
+ destroyed), so kept containers are sent as `{ id: <item_id> }` and new ones as
275
+ `{ fileContainerId: }`. READ: `file_container_ids` now maps `items[].fileContainer.id`
276
+ (the fragment's actual selection, which now also selects the item `id`) instead of a
277
+ `fileContainers` doc key the API never returns — reads always came back empty before.
278
+ BREAKING (dead surface): the `FileField#fileContainers` accessor is gone, replaced by
279
+ `items`; no consumer existed in this gem or eco-helpers, and it could only ever return
280
+ empty against real API responses. `file_container_ids` / `file_container_ids=` keep
281
+ their contract. `ImageGallery` is untouched: its write shape is also invalid, but live
282
+ `ImageInput` attaches via `sourceId` (an `Enzyme::ImageReference::TempImage`, a
283
+ different upload pipeline than file containers) — needs its own design, tracked in the
284
+ DataField shape-asymmetry review.
285
+ - **`People#dirty?`** compares the people-id SET vs `original_doc` (falling back to the
286
+ `people{id}` nodes on both sides), not the raw `peopleIds` key — the backend can return
287
+ `peopleIds: null` on reads while still populating `people`/`cachedPeople`, so restating an
288
+ already-attached person id no longer registers as a change. Live-confirmed 2026-07-16
289
+ (act-gov TOOCS dry-run reported "Updated 6/6" for a no-op). Mirrors the `CrossReference`
290
+ fix for the identical disease; `as_input` now serialises the fallback-aware `people_ids`
291
+ reader instead of the raw doc key.
292
+ - `ContractorEntities` field: the reader read the staged write key (`contractors`), which read
293
+ responses never populate — so it always returned `[]` on a freshly-fetched page — and the
294
+ default leaf diff phantom-dirtied the field the moment any setter ran, even for a no-op
295
+ restatement or a clear on an already-empty field. Now reads `contractorEntities[].id` with
296
+ staged-write preference and compares effective id SETS against `original_doc` in `dirty?`
297
+ (the CrossReference fix template).
298
+ - `Query::Page` now accepts the `CommonPageUnion` document variables (`showHiddenData`,
299
+ `content`, `only_content`, `fields`) as call params, and `Compat::Pages#get` forwards
300
+ them (`pages.get(id, content: false)`). Previously `accepted_params :id` silently
301
+ dropped any override, so the document-level defaults could never be changed.
302
+ This matters for least-privilege accounts: `attachedFiles @include(if: $content)` is
303
+ preauthorized on the `files: download` ability, so an account without that grant could
304
+ not complete **any** page read — found live when the template-tests account
305
+ (`-space graphql_templates`) had every `pages.get` refused while a minimal hand-written
306
+ page query succeeded. `content: false` is the structure-only read (template tooling,
307
+ config audits) and needs no file grant. Defaults are unchanged — no caller impact.
308
+ - **Four `Input::LocationStructure` command bugs are fixed — `add(type, **kwargs)` was broken on
309
+ every path.** `UpdateCommand#name` / `#classificationIds` called a non-existent `super` (plus a
310
+ redundant `nil.empty?` guard), raising `NoMethodError`; both readers now fall back to the stored
311
+ `name` / `classificationIds` so they work with or without a `new*` override. `ApplyCommands` and
312
+ `Draft::AddCommands` mapped the `:reorder` command type through a typo'd `:reoder` key, so
313
+ `add(:reorder, ...)` raised `NameError: uninitialized constant ...::ReoderCommand` — reorder was
314
+ unreachable from either dispatcher. `Interface::LocationStructure::Command#set_values` called the
315
+ 0-arg reader instead of the writer (`#{key}=`), raising `ArgumentError` and breaking the whole
316
+ `#add` path for every command type, not only reorder. Adds `commands_mapping_spec` (18 examples)
317
+ covering all four. Note: the typo symbol `:reoder` is no longer in the dispatch table, so
318
+ `add(:reoder)` now raises `RuntimeError: Unknown command type: 'reoder'` instead of `NameError` —
319
+ not a breaking change, since `:reoder` never resolved to a real class either way.
320
+ - **`FieldConfig::People` sent two keys the server rejects** — `addViewableFields` /
321
+ `removeViewableFields`, removed from the schema in the 20260725 drift. Now dropped, and the six
322
+ keys that drift *added* are reachable: `add`/`removeAllowedUserGroupIds`,
323
+ `attachedPeoplePermissionsEnabled`/`Editable`/`Flags`, `applyAttachedPeoplePermissionsTo`. The
324
+ nested flags hash is filtered to the seven declared flags so a stray key cannot sink the command.
325
+ - **`FieldConfig::Date` could not set renewals** — `createEvent`, `remindMeIn`, `renews`,
326
+ `renewsEvery`, `renewsUnit`, `renewsUntil` were all unreachable. Renewals are how expiry-driven
327
+ registers (certifications, inductions, permits) are configured, so this was a functional hole
328
+ rather than a cosmetic one.
329
+ - **`EditFieldConfiguration` silently dropped `stageId`** — required to disambiguate a field on a
330
+ phased page, which made the builder unusable there; callers had to hand-build the command hash.
331
+ `addPermittedPersonSchemaIds` / `removePermittedPersonSchemaIds` were dropped the same way.
332
+ - **Connection node-type poisoning.** `Logic::Connection` declared `embeds_many :nodes, klass: :node_class`
333
+ on the shared base, so the v2 collection layer memoized the first-resolved node class onto the single
334
+ shared collection — the first connection instantiated in a process poisoned every other connection's
335
+ instance `nodes` typing (e.g. a `MinimalDashboard` connection returning `AuthLog` nodes), corrupting
336
+ `QueryConnection#each` pagination when >1 connection type is used per process. Each subclass now binds
337
+ its own concrete-class collection via the new `Logic::Connection.nodes_of` macro. Regression spec added.
338
+ - `Compat::Response` now validates at construction that its payload honours the response contract
339
+ (`success?`/`error?`/`error_doc`/`item`), raising `ArgumentError` with the missing methods
340
+ otherwise. Closes the duck-typing drift class behind the eco-helpers 3.2.19 KPI-counter
341
+ regression (a non-response object slipping through). +spec `spec/ecoportal/api/graphql/compat/response_spec.rb`.
342
+ - **`main`'s version constant no longer advertises a published release it is not.** `2dad1e4`
343
+ (2026-08-08) set `GRAPQL_VERSION` to `1.3.15` while raising the `ecoportal-api` floor to
344
+ `0.10.17`. But `1.3.15` had already shipped nine days earlier from `release/1.3.15` (tag
345
+ `v1.3.15`, on RubyGems since 2026-07-30), and that tag is **not** an ancestor of `main` — the
346
+ two lines forked at `21516a3` (1.3.14) and `main` has carried ~200 commits of 1.4.0-track work
347
+ since. So `main` was building a materially different tree under a published version number,
348
+ against this repo's "never ship a fix under a published version" convention. The constant was
349
+ restored to `1.3.14` — the last release `main`'s tree actually descends from — until this
350
+ release set it to `2.0.0`. The `0.10.17` floor itself is unaffected and ships with this release.
351
+
352
+ ### Removed (BREAKING)
353
+
354
+ - `WorkflowCommandInput` commands `editPage` and `editPageCreatorPermissions` — removed from the
355
+ live schema (2026-07-13) and now removed from the gem. `WorkflowCommand.build(:editPage, …)` /
356
+ `build(:editPageCreatorPermissions, …)` raise `ArgumentError` (unknown key). The
357
+ `DEPRECATED_KEYS` / `warn_deprecated` machinery on `Input::WorkflowCommand` was removed with them.
358
+ **Migrate to:** `editTemplateBasicSettings` (name/icon/settings/counter/tags/folders/creator
359
+ perms), `editTemplateDescriptions` (`description`), `editTemplateCreatorPermissions` (creator
360
+ perms). The old `editPage` `addOtherTags`/`removeOtherTags` keys have no `editTemplate*`
361
+ equivalent (dropped from schema). No downstream callers existed (eco-helpers / multi_org_api /
362
+ gem swept) so the removal is clean. See DECISIONS.md in the workflow-space project.
363
+ - **`Mutation::SmartFill::Generate` / `Mutation::SmartFill::SubmitFeedback` and
364
+ `Builder::Page#generate_smart_fill` / `#submit_smart_fill_feedback`** (breaking, but dead
365
+ code) — `smartFillGenerate` and `smartFillSubmitFeedback` **do not exist in the schema**, and
366
+ never did: the live mutation list has no `*fill*` mutation at all (checked against both the
367
+ 2026-07-13 and 2026-07-30 introspections). SmartFill exists only as *configuration*
368
+ (`WorkflowCommandInput`'s `add/edit/removeSmartFillConfig`, the `SmartFill*Configuration`
369
+ types) and as a **read** data field — `Base::Page::DataField::SmartFill` is unaffected and
370
+ stays. Nothing could have depended on the removed mutations because they always failed; if
371
+ page-level SmartFill triggering is needed, it has to be added to the platform first.
372
+
373
+ ### Documentation
374
+
375
+ - `ecoPortal_architecture/10_forces_workflow_builder.md`: operational rules for auditing force
376
+ bindings — a binding may target a field OR a section; `globalBindings` is a separate collection;
377
+ a field declaring `global_binding` is present at stage render regardless of its section/stage;
378
+ backup-YAML ObjectIds are binary so a text search for an id proves nothing.
379
+ - `ecoPortal_architecture/07_data_fields.md`: which subset of fields carries an identifier, and how
380
+ to measure coverage — only the QUESTION carries one; its satellites (comment, image, document,
381
+ recommended action) do not and are not expected to. The denominator is workbook questions
382
+ (`question_ref`), never the field count; measuring against all fields yields a meaningless ~16%.
383
+ - `.ai-assistance/projects/mns-template-lifecycle/WORKBOOK-FIELD-MAPPING.md` — the authoritative
384
+ workbook-to-template contract recovered verbatim from the 2026-08-08 session transcript:
385
+ `QuestionSetReference` → template_container_id (UI "UNIQUE IDENTIFIER"), `QuestionReference` →
386
+ field `description` (CONFIRMED), `QuestionInformation` → field tooltip (strip HTML entities),
387
+ scores ignored, section-vs-stage left ambiguous, plus the template-pairing procedure and the
388
+ DEV-site targeting note.
389
+ - `.ai-assistance/conventions/recovering-prior-context.md` — past sessions are greppable JSONL
390
+ transcripts on disk (35 sessions, 85 MB); search them before asking the developer something he may
391
+ already have answered.
14
392
 
15
393
  ## [1.3.15] - 2026-07-30
16
394
 
@@ -0,0 +1,97 @@
1
+ module Ecoportal::API::Common::GraphQL::Model::Diffable
2
+ # Diff strategy for models whose *array* attributes are leaf DATA, not nested
3
+ # `GraphQL::Model` children.
4
+ #
5
+ # Array attributes declared with `passarray` (tag lists, id lists, config lists)
6
+ # — and `embeds_many` id-collections written as a whole set (e.g. page `locations`)
7
+ # — are registered as *cascaded* attributes. The default `DiffService`:
8
+ # 1. strips cascaded keys from the flat classic diff
9
+ # (`doc_with_non_cascaded_attributes`), on the assumption that a later
10
+ # `diff_reduce` pass re-adds them by cascading into nested `GraphQL::Model`
11
+ # children; and
12
+ # 2. in `diff_reduce`, deletes the flat placeholder for every cascaded key and
13
+ # only re-adds it when the child object `is_a?(GraphQL::Model)`.
14
+ # A `passarray` is wrapped as a v2 `ArrayModel` and a whole-set `embeds_many` as a
15
+ # `CollectionModel` — neither is a `GraphQL::Model`, so the re-add never happens and
16
+ # the array change becomes **invisible to `as_update`** (scalar `passthrough` fields
17
+ # are unaffected). This is the systemic "array/leaf fields don't diff" root cause
18
+ # (see `.ai-assistance/code/model_input_mapping/SYNTHESIS.md` §3.1).
19
+ #
20
+ # This service fixes it for leaf models by:
21
+ # 1. keeping cascaded keys in the flat classic diff (`doc_with_non_cascaded_attributes`
22
+ # is a no-op), and
23
+ # 2. overriding `diff_reduce` so the flat-diff placeholder for a NON-model leaf
24
+ # child is preserved (only real `GraphQL::Model` children are cascade-diffed;
25
+ # `root!` children are still dropped as independent entities), and
26
+ # 3. treating a leaf array as an OPAQUE value: a change replaces the whole array
27
+ # (a full-set replace), rather than the id-keyed nested update/delete/create
28
+ # patch-ops the default array diff would emit. This matches how the write inputs
29
+ # consume these arrays (`PageInput.otherTags`, `PageInput.locations = [{ id: }]`,
30
+ # register `filterTags`, PresetView `fieldConfigurations`).
31
+ #
32
+ # A model can either behave as a full leaf (call `dirty?`/`as_update` with
33
+ # `flat: true`, as `Base::Page::DataField` does — it has no nested `GraphQL::Model`
34
+ # children at all) or as a mixed model with both leaf arrays and real nested model
35
+ # children (page / register / preset view — the default cascaded `as_update` still
36
+ # recurses into the model children while the leaf arrays now diff inline).
37
+ class LeafDiffService < DiffService
38
+ private
39
+
40
+ # Keep all doc keys: leaf-field arrays are data, not nested models, so the flat
41
+ # classic diff must see them (the default strips every cascaded key).
42
+ def doc_with_non_cascaded_attributes(json)
43
+ json
44
+ end
45
+
46
+ # Treat array values as opaque leaves: emit the whole (current) array on any
47
+ # change, instead of the default id-keyed nested-array patch-ops. Non-array
48
+ # values fall through to the standard diff.
49
+ def change_diff(curr, prev, ignore: [])
50
+ return super unless curr.is_a?(Array) || prev.is_a?(Array)
51
+ return NO_CHANGES if same_array?(curr, prev)
52
+
53
+ keys_to_sym_deep(curr)
54
+ end
55
+
56
+ # Full-set equality (order-insensitive), mirroring `patch_data_flat_array`.
57
+ def same_array?(curr, prev)
58
+ curr ||= []
59
+ prev ||= []
60
+ curr.length == prev.length && (curr & prev).length == prev.length
61
+ end
62
+
63
+ # Preserve the flat-diff placeholder for non-model leaf children; only
64
+ # cascade-diff real `GraphQL::Model` children, and still drop `root!` children.
65
+ def diff_reduce(init, ignore: [])
66
+ subject.cascaded_reduce(init, recurs: false) do |result, obj, _key, key_path, _trace|
67
+ next result if key_path.empty?
68
+ next result if obj == subject
69
+
70
+ sym_path = key_path.map(&:to_sym)
71
+
72
+ unless cascadeable_model?(obj)
73
+ # Leaf array (ArrayModel / CollectionModel) → keep the flat-diff value.
74
+ # A root! child is not mutation payload → drop its placeholder.
75
+ dig_delete!(result, sym_path) if root?(obj) && result && dig_path?(result, sym_path)
76
+ next result
77
+ end
78
+
79
+ # Remove the flat-diff placeholder before the child computes its own diff.
80
+ dig_delete!(result, sym_path) if result && dig_path?(result, sym_path)
81
+
82
+ value = obj.as_update(ignore: ignore)
83
+ next result if value.nil?
84
+
85
+ result ||= {}
86
+ result[:id] ||= get_id(keys_to_sym_deep(curr_doc(flat: true) || {}), exception: false)
87
+ dig_set!(result, sym_path, value)
88
+ result
89
+ end
90
+ end
91
+
92
+ # A child worth cascading into: a non-root GraphQL::Model.
93
+ def cascadeable_model?(obj)
94
+ obj.is_a?(Ecoportal::API::Common::GraphQL::Model) && !root?(obj)
95
+ end
96
+ end
97
+ end
@@ -7,6 +7,7 @@ module Ecoportal
7
7
  require_relative 'diffable/hash_diff_nesting'
8
8
  require_relative 'diffable/classic_diff_service'
9
9
  require_relative 'diffable/diff_service'
10
+ require_relative 'diffable/leaf_diff_service'
10
11
 
11
12
  DIFF_CLASS = DiffService
12
13
 
@@ -29,7 +29,7 @@ module Ecoportal
29
29
  embeds_one :createdAt, klass: Base::DateTime
30
30
  embeds_one :updatedAt, klass: Base::DateTime
31
31
  embeds_one :completedAt, klass: Base::DateTime
32
- embeds_one :openededAt, klass: Base::DateTime
32
+ embeds_one :openedAt, klass: Base::DateTime
33
33
  embeds_one :closedAt, klass: Base::DateTime
34
34
  embeds_one :dueOrClosedDate, klass: Base::DateTime
35
35
 
@@ -0,0 +1,23 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # A scheduled/recurring occurrence of an action (calendar-style event view).
6
+ # Read-only. `occursAt` is an ISO8601 timestamp scalar (a plain string),
7
+ # not the `{ dateTime:, timeZone: }` DateTime object used elsewhere.
8
+ #
9
+ # status is exposed as a plain string ('open' | 'closed').
10
+ class ActionEvent < Logic::BaseModel
11
+ read_only!
12
+
13
+ passkey :id
14
+ passthrough :uid
15
+ passthrough :title, :subtitle, :description
16
+ passthrough :status, :relativeStatus
17
+ passthrough :occursAt
18
+ passboolean :archived, :recurring, :standaloneAction
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # The page (and stages) an ActionEvent is attached to. Read-only.
6
+ class ActionEventPage < Logic::BaseModel
7
+ read_only!
8
+
9
+ passkey :id
10
+ passthrough :name, :fieldId
11
+ passarray :stageIds
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # A single entry of a page's activity/history feed, returned by
6
+ # `page.activities` (ActivityInterfaceConnection). Read-only.
7
+ #
8
+ # `ActivityInterface` is an INTERFACE with ~61 concrete implementations
9
+ # (ActivityPageChangelog, ActivityComment, ActivityStageAssigned,
10
+ # ActivityCreated, ActivityEmailOut, ActivityFallback, ...). Rather than
11
+ # hand-model all of them, this captures the fields COMMON to the interface
12
+ # and passes the concrete `__typename` through so callers can branch on it.
13
+ #
14
+ # Enum-valued fields are exposed as plain strings:
15
+ # - priority: PriorityEnum
16
+ class Activity < Logic::BaseModel
17
+ read_only!
18
+
19
+ passkey :id
20
+ # Concrete type name (e.g. 'ActivityPageChangelog', 'ActivityComment', 'ActivityFallback').
21
+ passthrough :__typename
22
+ passthrough :priority
23
+ passthrough :stageId
24
+ passthrough :timeZone
25
+ # Origin of the activity — sourceId/sourceName are scalars; the polymorphic
26
+ # `source: SourceUnion` (BasicPage|PhasedPage) subtree is intentionally omitted.
27
+ passthrough :sourceId, :sourceName
28
+ passthrough :userId, :userEmail, :userName
29
+ passarray :categories, :subkey
30
+ passboolean :system, :fromPublicTemplate
31
+
32
+ embeds_one :createdAt, klass: Base::DateTime
33
+ embeds_one :updatedAt, klass: Base::DateTime
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end