ecoportal-api-graphql 1.3.9 → 1.3.11

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/code/diff_pairing_engine.md +243 -0
  3. data/.ai-assistance/code/graphql_domain_knowledge.md +20 -10
  4. data/.ai-assistance/code/template_diff_pairing_domain.md +175 -0
  5. data/.ai-assistance/code/workflow-command-guide.md +28 -0
  6. data/.ai-assistance/projects/TODO.md +21 -0
  7. data/.ai-assistance/projects/ooze-graphql-native-migration/INVENTORY.md +136 -0
  8. data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +6 -1
  9. data/.ai-assistance/projects/qa-services-delivery/DECISIONS.md +93 -0
  10. data/.ai-assistance/projects/qa-services-delivery/INTENT.md +76 -0
  11. data/.ai-assistance/projects/qa-services-delivery/PHASE3-SCOPE.md +115 -0
  12. data/.ai-assistance/projects/qa-services-delivery/ROADMAP.md +99 -0
  13. data/.ai-assistance/projects/qa-services-delivery/TODO.md +81 -0
  14. data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +77 -0
  15. data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +97 -0
  16. data/.ai-assistance/projects/template-diff-deploy/INTENT.md +12 -0
  17. data/.ai-assistance/projects/template-diff-deploy/TODO.md +9 -0
  18. data/.ai-assistance/projects/template-maintenance/PHASE0-FINDINGS.md +93 -0
  19. data/.ai-assistance/projects/template-maintenance/README.md +14 -0
  20. data/CHANGELOG.md +116 -0
  21. data/docs/worklog.md +408 -0
  22. data/ecoportal-api-graphql.gemspec +1 -1
  23. data/lib/ecoportal/api/graphql/base/page/data_field.rb +1 -1
  24. data/lib/ecoportal/api/graphql/builder/template_builder.rb +174 -0
  25. data/lib/ecoportal/api/graphql/builder.rb +17 -16
  26. data/lib/ecoportal/api/graphql/diff/change.rb +59 -0
  27. data/lib/ecoportal/api/graphql/diff/command_synthesizer.rb +329 -0
  28. data/lib/ecoportal/api/graphql/diff/cross_object_diff.rb +165 -0
  29. data/lib/ecoportal/api/graphql/diff/deploy.rb +121 -0
  30. data/lib/ecoportal/api/graphql/diff/id_resolver.rb +64 -0
  31. data/lib/ecoportal/api/graphql/diff/pairing/candidate.rb +32 -0
  32. data/lib/ecoportal/api/graphql/diff/pairing/engine.rb +173 -0
  33. data/lib/ecoportal/api/graphql/diff/pairing/ledger.rb +119 -0
  34. data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +104 -0
  35. data/lib/ecoportal/api/graphql/diff/strategy.rb +113 -0
  36. data/lib/ecoportal/api/graphql/diff/version_diff.rb +332 -0
  37. data/lib/ecoportal/api/graphql/diff.rb +34 -0
  38. data/lib/ecoportal/api/graphql/fragment/location_draft.rb +53 -53
  39. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +1 -0
  40. data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +27 -18
  41. data/lib/ecoportal/api/graphql/mutation/action/archive.rb +1 -1
  42. data/lib/ecoportal/api/graphql/mutation/action/create.rb +1 -1
  43. data/lib/ecoportal/api/graphql/mutation/action/update.rb +1 -1
  44. data/lib/ecoportal/api/graphql/mutation/contractor_entity/create.rb +1 -1
  45. data/lib/ecoportal/api/graphql/mutation/contractor_entity/destroy.rb +1 -1
  46. data/lib/ecoportal/api/graphql/mutation/contractor_entity/update.rb +1 -1
  47. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +1 -1
  48. data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +1 -1
  49. data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +1 -1
  50. data/lib/ecoportal/api/graphql/query/location_structure/draft.rb +62 -62
  51. data/lib/ecoportal/api/graphql/query/location_structure.rb +61 -61
  52. data/lib/ecoportal/api/graphql.rb +139 -138
  53. data/lib/ecoportal/api/graphql_version.rb +1 -1
  54. data/tests/dump_template_model.rb +90 -0
  55. data/tests/validate_queries.rb +127 -0
  56. metadata +32 -3
@@ -1,62 +1,62 @@
1
- module Ecoportal
2
- module API
3
- class GraphQL
4
- module Query
5
- class LocationStructure
6
- class Draft < Logic::Query
7
- accepted_params :id
8
- accepted_params :structureId
9
- accepted_params :includeArchivedNodes, default: true
10
-
11
- field_name :draft
12
-
13
- class_resolver :item_class, Model::LocationStructure::Draft
14
-
15
- private
16
-
17
- def basic_block(&block)
18
- final_block = block || default_query_block
19
- proc {
20
- query(
21
- id: :id!,
22
- structureId: :id!,
23
- includeArchivedNodes: :boolean
24
- ) {
25
- currentOrganization {
26
- locations {
27
- structure(
28
- id: :structureId
29
- ) {
30
- id
31
- name
32
- archived
33
- updatedAt
34
- draft(
35
- id: :id,
36
- &final_block
37
- )
38
- }
39
- }
40
- }
41
- }
42
- }
43
- end
44
-
45
- def default_query_block
46
- proc {
47
- spread :LocationDraft
48
- structure {
49
- nodes(
50
- includeArchived: :includeArchivedNodes
51
- ) {
52
- spread :LocationNode
53
- }
54
- }
55
- }
56
- end
57
- end
58
- end
59
- end # Query
60
- end # GraphQL
61
- end # API
62
- end # Ecoportal
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Query
5
+ class LocationStructure
6
+ class Draft < Logic::Query
7
+ accepted_params :id
8
+ accepted_params :structureId
9
+ accepted_params :includeArchivedNodes, default: true
10
+
11
+ field_name :draft
12
+
13
+ class_resolver :item_class, Model::LocationStructure::Draft
14
+
15
+ private
16
+
17
+ def basic_block(&block)
18
+ final_block = block || default_query_block
19
+ proc {
20
+ query(
21
+ id: :id!,
22
+ structureId: :id!,
23
+ includeArchivedNodes: :boolean
24
+ ) {
25
+ currentOrganization {
26
+ locations {
27
+ structure(
28
+ id: :structureId
29
+ ) {
30
+ id
31
+ name
32
+ archived
33
+ updatedAt { dateTime timeZone }
34
+ draft(
35
+ id: :id,
36
+ &final_block
37
+ )
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ end
44
+
45
+ def default_query_block
46
+ proc {
47
+ spread :LocationDraft
48
+ structure {
49
+ nodes(
50
+ includeArchived: :includeArchivedNodes
51
+ ) {
52
+ spread :LocationNode
53
+ }
54
+ }
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end # Query
60
+ end # GraphQL
61
+ end # API
62
+ end # Ecoportal
@@ -1,61 +1,61 @@
1
- module Ecoportal
2
- module API
3
- class GraphQL
4
- module Query
5
- class LocationStructure < Logic::Query
6
- accepted_params :id
7
- accepted_params :includeArchivedNodes, default: true
8
-
9
- field_name :structure
10
-
11
- class_resolver :item_class, Model::LocationStructure
12
-
13
- private
14
-
15
- def basic_block(&block)
16
- final_block = block || default_query_block
17
- proc {
18
- query(
19
- id: :id!,
20
- includeArchivedNodes: :boolean
21
- ) {
22
- currentOrganization {
23
- locations {
24
- structure(id: :id, &final_block)
25
- }
26
- }
27
- }
28
- }
29
- end
30
-
31
- # At the moment it always retrieves archived nodes!!
32
- # @note this is on purpose, as via API
33
- # there isn't much sense in not including archived nodes.
34
- def default_query_block
35
- proc {
36
- id
37
- name
38
- archived
39
- weight
40
- updatedAt
41
- visitorManagementEnabled
42
- nodes(
43
- includeArchived: :includeArchivedNodes
44
- ) {
45
- spread :LocationNode
46
- }
47
- drafts {
48
- id
49
- createdAt
50
- name
51
- notes
52
- }
53
- }
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
60
-
61
- require_relative 'location_structure/draft'
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Query
5
+ class LocationStructure < Logic::Query
6
+ accepted_params :id
7
+ accepted_params :includeArchivedNodes, default: true
8
+
9
+ field_name :structure
10
+
11
+ class_resolver :item_class, Model::LocationStructure
12
+
13
+ private
14
+
15
+ def basic_block(&block)
16
+ final_block = block || default_query_block
17
+ proc {
18
+ query(
19
+ id: :id!,
20
+ includeArchivedNodes: :boolean
21
+ ) {
22
+ currentOrganization {
23
+ locations {
24
+ structure(id: :id, &final_block)
25
+ }
26
+ }
27
+ }
28
+ }
29
+ end
30
+
31
+ # At the moment it always retrieves archived nodes!!
32
+ # @note this is on purpose, as via API
33
+ # there isn't much sense in not including archived nodes.
34
+ def default_query_block
35
+ proc {
36
+ id
37
+ name
38
+ archived
39
+ weight
40
+ updatedAt { dateTime timeZone }
41
+ visitorManagementEnabled
42
+ nodes(
43
+ includeArchived: :includeArchivedNodes
44
+ ) {
45
+ spread :LocationNode
46
+ }
47
+ drafts {
48
+ id
49
+ createdAt
50
+ name
51
+ notes
52
+ }
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ require_relative 'location_structure/draft'
@@ -1,138 +1,139 @@
1
- module Ecoportal
2
- module API
3
- # @attr_reader client [Common::GraphQL::Client] a client object that holds the configuration of the api connection.
4
- # @attr_reader logger [Logger] the logger.
5
- class GraphQL
6
- include Ecoportal::API::Common::GraphQL::ClassHelpers
7
-
8
- class_resolver :client_class, Ecoportal::API::Common::GraphQL::Client
9
-
10
- attr_reader :client, :fragments
11
-
12
- # Creates a `GraphQL` object to interact with the ecoPortal `GraphQL API`.
13
- # @param org_id [String] the id of the target organization.
14
- # It defaults to the environmental variable `ORGANIZATION_ID`, if defined
15
- # @param logger [Logger] an object with `Logger` interface to generate logs.
16
- def initialize(email: nil, pass: nil, org_id: nil, host: "live.ecoportal.com")
17
- kargs = {
18
- email: email,
19
- pass: pass,
20
- host: host,
21
- org_id: org_id,
22
- no_schema: true
23
- }
24
-
25
- @client = client_class.new(**kargs)
26
- @client.http_client = Ecoportal::API::Common::GraphQL::HttpClient.new(
27
- email: email,
28
- pass: pass,
29
- org_id: org_id,
30
- host: host
31
- )
32
- @fragments = Ecoportal::API::GraphQL::Fragment.new(client)
33
- end
34
-
35
- def currentOrganizationClass
36
- API::GraphQL::Model::Organization.tap do |org_class|
37
- org_class.client = client
38
- end
39
- end
40
-
41
- def currentOrganization
42
- currentOrganizationClass
43
- end
44
-
45
- def createContractorEntity(input:, &block)
46
- createContractorEntityMutation.query(input: input, &block)
47
- end
48
-
49
- def contractorEntity
50
- Ecoportal::API::GraphQL::Builder::ContractorEntity.new(client)
51
- end
52
-
53
- # Gives a builder to use different options to modify a reporting structure
54
- def locationStructure
55
- Ecoportal::API::GraphQL::Builder::LocationStructure.new(client)
56
- end
57
-
58
- # Gives a builder to use different options to play with action
59
- def action
60
- Ecoportal::API::GraphQL::Builder::Action.new(client)
61
- end
62
-
63
- # Gives a builder to use different options to work with pages
64
- def page
65
- Ecoportal::API::GraphQL::Builder::Page.new(client)
66
- end
67
-
68
- # v2-compatible pages API — exposes get/get_new/create/update/get_body
69
- # matching the interface eco-helpers scripts use against ecoportal-api-v2.
70
- def pages
71
- Compat::Pages.new(client)
72
- end
73
-
74
- # v2-compatible registers API — exposes search with cursor pagination.
75
- def registers
76
- Compat::Registers.new(client)
77
- end
78
-
79
- # Mutation builder for register CRUD + preset view management.
80
- # Distinct from #registers (compat search layer).
81
- #
82
- # Usage:
83
- # api.register.create(input: { name: 'Safety', moduleType: 'form', filterTags: [] })
84
- # api.register.preset_view.list('REG_ID')
85
- # api.register.preset_view.create(input: { registerId: 'R', name: 'Active', fieldConfigs: [] })
86
- def register
87
- Ecoportal::API::GraphQL::Builder::Register.new(client)
88
- end
89
-
90
- # Kickstand workflow engine controls — start/stop/fail individual workflows
91
- # or bulk-update a set. Internal back-end automation; not for customer scripts.
92
- def kickstand
93
- Builder::Kickstand.new(client)
94
- end
95
-
96
- # Template management — create/update/publish/unpublish templates and manage
97
- # template information and related pages.
98
- def template
99
- Builder::Template.new(client)
100
- end
101
-
102
- # File upload orchestrator — get S3 credentials, upload to S3, register, poll.
103
- # Returns a fileContainerId string usable in FileField/ImageGallery mutations.
104
- #
105
- # Usage:
106
- # id = api.file_upload.upload('/path/to/report.pdf')
107
- # page.components.get_by_name('Report').file_container_ids = [id]
108
- # api.pages.update(page)
109
- def file_upload
110
- Ecoportal::API::GraphQL::FileUpload::Client.new(self)
111
- end
112
-
113
- private
114
-
115
- def createContractorEntityMutation
116
- Ecoportal::API::GraphQL::Mutation::ContractorEntity::Create.new(client)
117
- end
118
- end
119
- end
120
- end
121
-
122
- require_relative 'graphql/helpers'
123
- require_relative 'graphql/concerns'
124
- require_relative 'graphql/logic/base_model'
125
- require_relative 'graphql/error'
126
- require_relative 'graphql/interface'
127
- require_relative 'graphql/base'
128
- require_relative 'graphql/model'
129
- require_relative 'graphql/logic'
130
- require_relative 'graphql/connection'
131
- require_relative 'graphql/payload'
132
- require_relative 'graphql/input'
133
- require_relative 'graphql/fragment'
134
- require_relative 'graphql/query'
135
- require_relative 'graphql/mutation'
136
- require_relative 'graphql/builder'
137
- require_relative 'graphql/compat'
138
- require_relative 'graphql/file_upload'
1
+ module Ecoportal
2
+ module API
3
+ # @attr_reader client [Common::GraphQL::Client] a client object that holds the configuration of the api connection.
4
+ # @attr_reader logger [Logger] the logger.
5
+ class GraphQL
6
+ include Ecoportal::API::Common::GraphQL::ClassHelpers
7
+
8
+ class_resolver :client_class, Ecoportal::API::Common::GraphQL::Client
9
+
10
+ attr_reader :client, :fragments
11
+
12
+ # Creates a `GraphQL` object to interact with the ecoPortal `GraphQL API`.
13
+ # @param org_id [String] the id of the target organization.
14
+ # It defaults to the environmental variable `ORGANIZATION_ID`, if defined
15
+ # @param logger [Logger] an object with `Logger` interface to generate logs.
16
+ def initialize(email: nil, pass: nil, org_id: nil, host: "live.ecoportal.com")
17
+ kargs = {
18
+ email: email,
19
+ pass: pass,
20
+ host: host,
21
+ org_id: org_id,
22
+ no_schema: true
23
+ }
24
+
25
+ @client = client_class.new(**kargs)
26
+ @client.http_client = Ecoportal::API::Common::GraphQL::HttpClient.new(
27
+ email: email,
28
+ pass: pass,
29
+ org_id: org_id,
30
+ host: host
31
+ )
32
+ @fragments = Ecoportal::API::GraphQL::Fragment.new(client)
33
+ end
34
+
35
+ def currentOrganizationClass
36
+ API::GraphQL::Model::Organization.tap do |org_class|
37
+ org_class.client = client
38
+ end
39
+ end
40
+
41
+ def currentOrganization
42
+ currentOrganizationClass
43
+ end
44
+
45
+ def createContractorEntity(input:, &block)
46
+ createContractorEntityMutation.query(input: input, &block)
47
+ end
48
+
49
+ def contractorEntity
50
+ Ecoportal::API::GraphQL::Builder::ContractorEntity.new(client)
51
+ end
52
+
53
+ # Gives a builder to use different options to modify a reporting structure
54
+ def locationStructure
55
+ Ecoportal::API::GraphQL::Builder::LocationStructure.new(client)
56
+ end
57
+
58
+ # Gives a builder to use different options to play with action
59
+ def action
60
+ Ecoportal::API::GraphQL::Builder::Action.new(client)
61
+ end
62
+
63
+ # Gives a builder to use different options to work with pages
64
+ def page
65
+ Ecoportal::API::GraphQL::Builder::Page.new(client)
66
+ end
67
+
68
+ # v2-compatible pages API — exposes get/get_new/create/update/get_body
69
+ # matching the interface eco-helpers scripts use against ecoportal-api-v2.
70
+ def pages
71
+ Compat::Pages.new(client)
72
+ end
73
+
74
+ # v2-compatible registers API — exposes search with cursor pagination.
75
+ def registers
76
+ Compat::Registers.new(client)
77
+ end
78
+
79
+ # Mutation builder for register CRUD + preset view management.
80
+ # Distinct from #registers (compat search layer).
81
+ #
82
+ # Usage:
83
+ # api.register.create(input: { name: 'Safety', moduleType: 'form', filterTags: [] })
84
+ # api.register.preset_view.list('REG_ID')
85
+ # api.register.preset_view.create(input: { registerId: 'R', name: 'Active', fieldConfigs: [] })
86
+ def register
87
+ Ecoportal::API::GraphQL::Builder::Register.new(client)
88
+ end
89
+
90
+ # Kickstand workflow engine controls — start/stop/fail individual workflows
91
+ # or bulk-update a set. Internal back-end automation; not for customer scripts.
92
+ def kickstand
93
+ Builder::Kickstand.new(client)
94
+ end
95
+
96
+ # Template management — create/update/publish/unpublish templates and manage
97
+ # template information and related pages.
98
+ def template
99
+ Builder::Template.new(client)
100
+ end
101
+
102
+ # File upload orchestrator — get S3 credentials, upload to S3, register, poll.
103
+ # Returns a fileContainerId string usable in FileField/ImageGallery mutations.
104
+ #
105
+ # Usage:
106
+ # id = api.file_upload.upload('/path/to/report.pdf')
107
+ # page.components.get_by_name('Report').file_container_ids = [id]
108
+ # api.pages.update(page)
109
+ def file_upload
110
+ Ecoportal::API::GraphQL::FileUpload::Client.new(self)
111
+ end
112
+
113
+ private
114
+
115
+ def createContractorEntityMutation
116
+ Ecoportal::API::GraphQL::Mutation::ContractorEntity::Create.new(client)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ require_relative 'graphql/helpers'
123
+ require_relative 'graphql/concerns'
124
+ require_relative 'graphql/logic/base_model'
125
+ require_relative 'graphql/error'
126
+ require_relative 'graphql/interface'
127
+ require_relative 'graphql/base'
128
+ require_relative 'graphql/model'
129
+ require_relative 'graphql/logic'
130
+ require_relative 'graphql/connection'
131
+ require_relative 'graphql/payload'
132
+ require_relative 'graphql/input'
133
+ require_relative 'graphql/fragment'
134
+ require_relative 'graphql/query'
135
+ require_relative 'graphql/mutation'
136
+ require_relative 'graphql/builder'
137
+ require_relative 'graphql/compat'
138
+ require_relative 'graphql/file_upload'
139
+ require_relative 'graphql/diff'
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- GRAPQL_VERSION = '1.3.9'.freeze
3
+ GRAPQL_VERSION = '1.3.11'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,90 @@
1
+ require_relative 'local_libs'
2
+ require 'json'
3
+ require 'fileutils'
4
+
5
+ # Dump a TEMPLATE's GraphQL model to JSON, so a real template can be captured as the golden
6
+ # fixture for the template-editing pipeline (build-from-scratch + diff-and-update).
7
+ #
8
+ # A template IS a page in this API (Query::Templates returns PageUnion nodes), so its structural
9
+ # skeleton — stages -> sections -> components(fields) — comes from the ordinary page model, exactly
10
+ # like tests/dump_page_model.rb. On top of that, this tool also captures the page's *workflow
11
+ # command log* (Query::PagesWorkflowCommands) — the applied command history that drives the
12
+ # workflow builder (callbacks / operations / strategies / recipients config).
13
+ #
14
+ # Two artefacts are written per template:
15
+ # spec/fixtures/templates/<id>[_<label>].json -> structural page doc (golden master)
16
+ # spec/fixtures/templates/<id>[_<label>].commands.json -> workflow command log (if readable)
17
+ #
18
+ # Usage (run where the GraphQL connection is configured, like the other tests/):
19
+ # ruby tests/dump_template_model.rb <TEMPLATE_ID> [--out PATH] [--label before|after]
20
+ #
21
+ # Reference target (see .ai-assistance/projects/template-maintenance/DESIGN.md):
22
+ # ruby tests/dump_template_model.rb 6a3fa5b8f89e07c758df622b --label golden
23
+ #
24
+ # Run --label before / --label after around an executeWorkflowCommands batch to diff exactly
25
+ # what a command sequence changed — the same before/after workflow dump_page_model.rb enables.
26
+
27
+ def parse_args(argv)
28
+ opts = { template_id: nil, out: nil, label: nil }
29
+ rest = argv.dup
30
+ opts[:template_id] = rest.shift
31
+ until rest.empty?
32
+ flag = rest.shift
33
+ case flag
34
+ when '--out' then opts[:out] = rest.shift
35
+ when '--label' then opts[:label] = rest.shift
36
+ else
37
+ warn "Unknown argument: #{flag}"
38
+ end
39
+ end
40
+ opts
41
+ end
42
+
43
+ opts = parse_args(ARGV)
44
+
45
+ unless opts[:template_id]
46
+ warn 'Usage: ruby tests/dump_template_model.rb <TEMPLATE_ID> [--out PATH] [--label before|after]'
47
+ exit 1
48
+ end
49
+
50
+ default_dir = File.expand_path('../spec/fixtures/templates', __dir__)
51
+ default_name = [opts[:template_id], opts[:label]].compact.join('_')
52
+ struct_path = opts[:out] || File.join(default_dir, "#{default_name}.json")
53
+ commands_path = File.join(File.dirname(struct_path), "#{File.basename(struct_path, '.json')}.commands.json")
54
+
55
+ api = Ecoportal::API::GraphQL.new
56
+
57
+ # 1) Structural skeleton — a template is a page, so fetch it via the page model.
58
+ page = api.pages.get(opts[:template_id])
59
+
60
+ if page.nil?
61
+ warn "Could not fetch template '#{opts[:template_id]}' as a page"
62
+ exit 2
63
+ end
64
+
65
+ doc = page.respond_to?(:doc) ? page.doc : page.instance_variable_get(:@page)&.doc
66
+
67
+ FileUtils.mkdir_p(File.dirname(struct_path))
68
+ File.write(struct_path, JSON.pretty_generate(doc))
69
+
70
+ puts "Dumped template structure '#{opts[:template_id]}' -> #{struct_path}"
71
+ puts " name: #{page.respond_to?(:name) ? page.name : '(n/a)'}"
72
+ puts " patchVer: #{page.respond_to?(:patchVer) ? page.patchVer : '(n/a)'}"
73
+
74
+ # 2) Workflow command log — the applied-command history behind the workflow builder.
75
+ # Wrapped in a rescue: the PagesWorkflow read model is still partial (stages is a bare
76
+ # passarray as of SCHEMA_VERSION 20260605), so treat this as best-effort.
77
+ begin
78
+ query = Ecoportal::API::GraphQL::Query::PagesWorkflowCommands.new(api.client)
79
+ commands = []
80
+ query.each(id: opts[:template_id], first: 100) do |cmd|
81
+ commands << (cmd.respond_to?(:doc) ? cmd.doc : cmd)
82
+ end
83
+ File.write(commands_path, JSON.pretty_generate(commands))
84
+ puts "Dumped #{commands.size} workflow command(s) -> #{commands_path}"
85
+ rescue StandardError => e
86
+ warn "Workflow command log not captured (#{e.class}: #{e.message})"
87
+ warn ' -> structural fixture is still valid; the command log read may be WIP for this org.'
88
+ end
89
+
90
+ puts 'Tip: capture --label before and --label after an executeWorkflowCommands batch, then diff the JSON.'