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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c1990322b8462a243436be2fbd9b7194512fa2ce5222a1fa5d601e248a67e4f
4
- data.tar.gz: 5ed9647580308227e46a57f00fd29db20603dc66f221167630205a9fffe94ae4
3
+ metadata.gz: 78e2adbcdb62b6d2fdbf2ddb5906d9d8b5b93464af6269936b7eeda2dc94cb17
4
+ data.tar.gz: e6c55777dab78795abe7b81f419275cb52ec82a986aa92ee0da4affa4d5ca7e8
5
5
  SHA512:
6
- metadata.gz: 6bd2a2fc3852bfa39f5923f0fa2a5b1a63a0fc639454b9d15ea8bbdee8276aa1a2fe7b3ddc625687a9d99e8fcb3497c7760c675390782b518a2b0a35eb9edf76
7
- data.tar.gz: 01bbe1985ea4569504d703fe2ed9a93d06783e96c6e1accf9159d1e9973cf2a5cf5930c23a2d73b9eb5d79ccc00f58bd6ad6bb6ef5db7ca3686673ea5a0f0410
6
+ metadata.gz: 2bff88bcf06b31025a011fdb2b7d5f4a209aaa92b21c4c6c09ef1439da39a0b1e76fe75a204e5d4bc3e55dda5b7d4d6b215554650007b6d5097afa388ca08537
7
+ data.tar.gz: 0df6a4578b1ddbefa765f813fd97efd5f8e55fc84ad0384a7e7e6459f2ddc040dc6d8999af2493bb4e6368a638254ca7d8ed2154b7eae27bc40b965b80a94be4
@@ -0,0 +1,243 @@
1
+ # Diff — Command Synthesis, Deploy & Pairing Engine
2
+
3
+ **Area:** `lib/ecoportal/api/graphql/diff/` · **Status:** built (gem branch
4
+ `feature/template-diff-deploy-pairing`) · **Domain reference (read first):**
5
+ `template_diff_pairing_domain.md`.
6
+
7
+ This doc covers the diff → command → deploy pipeline and the cross-object pairing engine + learning
8
+ ledger. `VersionDiff`/`Change` (the self-version structural diff) are covered inline in their files.
9
+
10
+ ---
11
+
12
+ ## 1. The pipeline
13
+
14
+ ```
15
+ pair (id | genome | type+label | ledger) Diff::Pairing::Engine (+ Ledger)
16
+ └─► diff on paired model Diff::VersionDiff → [Change]
17
+ └─► delta as WorkflowCommands Diff::CommandSynthesizer (+ IdResolver)
18
+ └─► apply (gated on unsupported) Diff::Deploy#execute!
19
+ ```
20
+
21
+ Self-version (same object, retained ids) needs NO pairing — `VersionDiff` matches by id and the
22
+ synthesiser replays as-is. Cross-object (UAT↔PROD) needs pairing to translate the ids first.
23
+
24
+ ---
25
+
26
+ ## 2. Classes
27
+
28
+ | Class | File | Role |
29
+ |---|---|---|
30
+ | `Diff::Strategy` | `diff/strategy.rb` | Composable diff config (pairing × scope × move-sensitivity × intent); `#filter(changes)` |
31
+ | `Diff::VersionDiff` | `diff/version_diff.rb` | Self-version (`:id` pairing) front-end → `[Change]`; accepts `strategy:` |
32
+ | `Diff::CrossObjectDiff` | `diff/cross_object_diff.rb` | Cross-object front-end: pairs fields via `Pairing::Engine`, emits same `[Change]` |
33
+ | `Diff::Change` | `diff/change.rb` | One change (op/kind/id/before/after/parent_id) (unchanged) |
34
+ | `Diff::CommandSynthesizer` | `diff/command_synthesizer.rb` | `[Change]` → ordered built `WorkflowCommand` batch + `unsupported` |
35
+ | `Diff::IdResolver` | `diff/id_resolver.rb` | Maps `resolve(kind, human-key) => id` (stage name / section heading → target id) |
36
+ | `Diff::Deploy` | `diff/deploy.rb` | Orchestrates diff → commands → apply; gates on `unsupported` |
37
+ | `Diff::Pairing::Signals` | `diff/pairing/signals.rb` | Weak signals (genome/type/label/options), each 0.0..1.0 or nil |
38
+ | `Diff::Pairing::Candidate` | `diff/pairing/candidate.rb` | Scored source↔target proposal (`score`, `signals`, `matched_by`) |
39
+ | `Diff::Pairing::Engine` | `diff/pairing/engine.rb` | Multi-signal equivalence matcher → accepted / ambiguous / unmatched |
40
+ | `Diff::Pairing::Ledger` | `diff/pairing/ledger.rb` | Persisted confirmed equivalences (JSON); consulted first |
41
+
42
+ ---
43
+
44
+ ## 3. CommandSynthesizer — edit mode
45
+
46
+ `CommandSynthesizer.new(changes, resolver: nil)`. Emits in a dependency-safe order (stages → sections
47
+ → fields → options; within a kind removes → adds → moves → edits). A `command_for` result may be a
48
+ single hash, an **Array** of hashes (a move that emits remove+add), or nil (unsupported); `build_all`
49
+ normalises via `wrap` (NOT `Kernel#Array`, which would splat a Hash).
50
+
51
+ | Change | Command(s) | Notes |
52
+ |---|---|---|
53
+ | stage added/removed/moved | `addStage`/`removeStage`/`moveStage` | |
54
+ | stage `name` changed | `editStage(stageId, name)` | |
55
+ | section added/removed | `addSection`/`removeSection` | |
56
+ | section `heading` changed | `editSectionHeader(sectionId, header)` | |
57
+ | **section `stage` moved** | `removeStageSection` + `addStageSection` | **needs resolver** for stage ids; else UNSUPPORTED |
58
+ | field added/removed | `addField`/`removeField` | |
59
+ | field `label` changed | `editFieldConfiguration(dataFieldId, label)` | |
60
+ | **field `section` moved** | `moveField(id, sectionId)` | **needs resolver** for section id; else UNSUPPORTED |
61
+ | field `type` changed | — | **UNSUPPORTED** (no `editFieldType`; a rebuild would drop data) |
62
+ | option added/removed/changed(label,weight) | `add/remove/editSelectFieldOption` | uses `change.parent_id` (field id); nil → UNSUPPORTED |
63
+ | **field_config changed** (typed byType) | `editFieldConfiguration(byType: { <key> => { <attr> => after } })` | `change.by_type` names the sub-hash (`:gauge`/`:select`/`:date`); nil by_type → UNSUPPORTED. Emitted only for confirmed read↔write-key matches (see VersionDiff) |
64
+ | **gauge_stop added** | `addGaugeFieldStop(dataFieldId, threshold, color)` | threshold+color carried on `change.after`; `parent_id` = gauge field id |
65
+ | **gauge_stop removed** | `removeGaugeFieldStop(dataFieldId, stopId)` | `parent_id` = gauge field id |
66
+ | **gauge_stop changed** (threshold\|color) | `editGaugeFieldStop(dataFieldId, stopId, <attr>)` | one command per changed attr |
67
+
68
+ **Resolver contract:** any object answering `resolve(kind, key) => id | nil`. Without one, moves stay
69
+ UNSUPPORTED — the synthesiser NEVER guesses a target id. `IdResolver.from_doc(target_doc)` builds one
70
+ by indexing stage names + section headings; **ambiguous keys (duplicates) resolve to nil** (safe).
71
+
72
+ ---
73
+
74
+ ## 4. Deploy
75
+
76
+ ```ruby
77
+ plan = Diff::Deploy.from_versions(before_doc, after_doc) # self-version replay
78
+ plan = Diff::Deploy.from_versions(uat_v1, uat_v2, target_doc: prod_doc) # cross-object: moves resolve vs PROD
79
+ plan.commands # ordered built command hashes, ready for executeWorkflowCommands
80
+ plan.unsupported # [Change] needing human handling (never guessed)
81
+ plan.changelog # human one-liners
82
+ plan.execute!(page) # sends via page.execute_workflow_commands; RAISES if unsupported unless allow_partial: true
83
+ ```
84
+
85
+ Inert until `execute!` with an explicit executor. `from_versions` derives an `IdResolver` from
86
+ `target_doc:` (override with `resolver:`).
87
+
88
+ ---
89
+
90
+ ## 5. Pairing engine (equivalence matching)
91
+
92
+ Cross-object pairing is entity-resolution, not lookup (no shared Mongo ids). `Engine#pair(sources,
93
+ targets)` returns `Result(accepted, ambiguous, unmatched)`:
94
+
95
+ 1. **Ledger first** — a previously-confirmed pair auto-resolves (`matched_by: :ledger`, score 1.0),
96
+ consuming the target so it can't be re-assigned. Pairing improves over time.
97
+ 2. **Score the rest** — every remaining source×target via `Signals`, weighted mean over the signals
98
+ that APPLY (nil signals excluded so a select field isn't penalised for absent genome).
99
+ 3. **Greedy one-to-one assignment**, strongest links first.
100
+ 4. **Classify:** `accepted` (score ≥ `accept_threshold` 0.85 AND not within `tie_margin` 0.1 of the
101
+ runner-up) · `ambiguous` (0.5–0.85, or a near-tie) · `unmatched` (< 0.5 review_threshold).
102
+
103
+ **Signals + weights:** genome 0.5, type 0.2, label 0.2, options 0.1.
104
+ - **genome** — match 1.0, mismatch 0.0 (does NOT veto — a field may be re-purposed keeping its
105
+ genome, or genome may be absent on newer types), nil when neither side carries one. Strong but
106
+ **fallible** (6 documented failure modes — see domain ref §5).
107
+ - **type** — `__typename` equality (a type change disqualifies a data pairing).
108
+ - **label** — case/space-insensitive exact = 1.0, else token Jaccard (partial credit for a re-purpose
109
+ candidate like `Date logged` → `Date of sign-off`).
110
+ - **options** — select options overlap by **value** (primary) / label — the correct option identity,
111
+ never genome; nil for non-select fields.
112
+
113
+ **Never guesses:** only `accepted` is safe to auto-apply/record; `ambiguous`+`unmatched` are surfaced
114
+ for a human. A same-label/type field whose genome CONTRADICTS is pulled below accept (→ ambiguous or
115
+ unmatched), never auto-paired.
116
+
117
+ ---
118
+
119
+ ## 6. Learning ledger (first-class artifact)
120
+
121
+ `Ledger` records only CONFIRMED equivalences (auto-accepted high-confidence or human-adjudicated),
122
+ keyed by `(kind, source_id)`, storing `target_id` + `matched_by` + `confidence` + `signals` +
123
+ `recorded_at`. `Engine#confirm!(candidate, matched_by: :human)` writes back. JSON round-trips via
124
+ `Ledger.load(path)` / `#save`; a missing file yields an empty writable ledger; a later record
125
+ supersedes an earlier one (human corrects an auto-accept). The how-resolved log is the interim bridge
126
+ data Product's Field-ID / template-entity-id effort needs.
127
+
128
+ > Note the struct member is `matched_by`, NOT `method` — `method` would shadow `Object#method`
129
+ > (`Lint/StructNewOverride`). The `Ledger#record` / `Engine#confirm!` keyword is likewise `matched_by:`.
130
+
131
+ ---
132
+
133
+ ## 7. Gauge stops + typed byType config emission (round #3 — DONE)
134
+
135
+ `VersionDiff` now emits two new `Change` kinds (id-paired, self-version):
136
+
137
+ - **`:gauge_stop`** — diffs each Gauge field's `stops` list (`{ id, threshold, color }`, matched by
138
+ retained stop id): added (carries `after: { threshold:, color: }`), removed, changed
139
+ (`threshold`/`color`). `parent_id` = the gauge field id.
140
+ - **`:field_config`** — typed per-field configuration changes, `parent_id` = field id and `by_type`
141
+ = the `editFieldConfiguration.byType` sub-hash key. Emission is **data-driven + conservative**
142
+ (`VersionDiff::BYTYPE_CONFIG`): only where the READ doc property name matches the WRITE byType
143
+ input key EXACTLY. Currently confirmed against the schema + input classes:
144
+ - `Gauge` → `{ gauge: [max] }`
145
+ - `Select` → `{ select: [dataType, multiple, flat, other, otherDesc] }`
146
+ - `Date` → `{ date: [showTime, pastOnly, todayButton] }`
147
+ Anything else (e.g. PlainText `multiline` — not a `plainText` byType key; `required` — no
148
+ editFieldConfiguration key at all) is **not emitted** (unmappable → never fabricated).
149
+
150
+ `Stop` shape and the gauge byType `max` verified against
151
+ `.ai-assistance/tmp/20260605T101224_live_ep_graphql_schema.graphql.json` (`Stop { id, threshold: Float,
152
+ color: String }`; `WorkflowEditFieldConfigurationGaugeInput { max: Float }`).
153
+
154
+ ## 7b. placeholderId threading (dependent intra-batch creates — round #3 — DONE)
155
+
156
+ `CommandSynthesizer.new(changes, thread_placeholders:)` (Deploy passes `true` by default). When on,
157
+ every ADDED structural node (stage/section/field) is assigned a deterministic client-chosen
158
+ `placeholderId` keyed by its source id; `addStage`/`addSection`/`addField` emit that placeholder, and
159
+ any later command in the SAME batch that references such a node (`addSelectFieldOption` /
160
+ `add/edit/removeGaugeFieldStop` / `editFieldConfiguration` dataFieldId) is rewritten to the
161
+ placeholder instead of the target-invalid source id (`ref(id)` helper). References to pre-existing
162
+ nodes keep their real id. Threading OFF → empty map → the source ids pass through unchanged (the
163
+ prior self-version behaviour). This makes add-then-reference sequences self-consistent within one
164
+ `executeWorkflowCommands` call. The placeholderId inputs are native to the command modules
165
+ (`addStage`/`addSection`/`addField` all list `placeholderId` in VALID_KEYS; `addGaugeFieldStop`
166
+ accepts an optional one too).
167
+
168
+ **Note on self-version scope:** a self-version `VersionDiff` subsumes a NEW field's options/config
169
+ into its single `added`-field change (children are not separately emitted when the parent is brand
170
+ new), so the natural self-version batch has no add-then-reference pair. Threading is therefore
171
+ latent for pure self-version replay and load-bearing for the CROSS-OBJECT / assembled change-set
172
+ replay (a field created in the batch referenced by later commands). Deploy is proven end-to-end
173
+ against such an assembled change-set.
174
+
175
+ ## 7c. What remains UNSUPPORTED / deferred
176
+
177
+ - **Field type change** — no schema command; would need remove+add (drops data) → always UNSUPPORTED.
178
+ - **Moves without a resolver** — field/section moves need a target id; nil resolver → UNSUPPORTED.
179
+ - **Non-confirmed byType config props** — anything outside `BYTYPE_CONFIG` (PlainText `multiline`,
180
+ `required`, RichText `markdown`/`content`, People/Table/CrossReference/etc. bodies): the read
181
+ fragment and/or the byType input keys were not confirmed to line up 1:1, so they are NOT emitted
182
+ (never fabricated). Extend `BYTYPE_CONFIG` per type once the read shape is confirmed.
183
+ - **Gauge stop reordering** — stops are matched by id (add/remove/attr change) but a pure reorder is
184
+ not modelled (no ordering field surfaced on `Stop`); left UNSUPPORTED.
185
+ - **Structural cross-references in emitted adds** — `addField`/`addSection` still emit only
186
+ label/placeholder (not `stageId`/`sectionId` back-refs to their added parent). Once those refs are
187
+ emitted, placeholder threading will thread them too (the primitive is already in place).
188
+ - **Interactive assisted-resolution UX + `TypedFieldsPairing` as an extra signal** — eco-helpers layer.
189
+
190
+ ## 8. Diff modalities — `Strategy` + `CrossObjectDiff` (Phase 4 — DONE)
191
+
192
+ Diffing is a COMPOSABLE FAMILY (domain ref §8), not one diff. `Diff::Strategy` is the value object
193
+ over four axes:
194
+
195
+ - **pairing** `:id` (self-version, default) · `:genome` · `:type_label` · `:assisted` (full engine).
196
+ `:id` needs no pairing map; the other three are `cross_object?` (need one).
197
+ - **scope** `:structural` (all) · `:config_only` (field_config/option/gauge_stop) · `:data_migration`
198
+ (field changes only).
199
+ - **move_sensitive** — false suppresses `:moved` changes; config_only/data_migration never emit moves.
200
+ - **intent** `:changelog` · `:deploy` · `:sync_readiness` (documentation; `:deploy` keeps threading on).
201
+
202
+ `Strategy#filter(changes)` applies scope + move-sensitivity to a computed change-set. `Strategy.default`
203
+ is `{id, structural, move-aware, changelog}` — EXACTLY the pre-Phase-4 `VersionDiff` behaviour (BC).
204
+
205
+ `VersionDiff.new(before, after, strategy:)` is the `:id` front-end: it computes the full id-paired
206
+ change-set once (`all_changes`) and returns `strategy.filter(all_changes)`. Default = unchanged.
207
+
208
+ `CrossObjectDiff.new(source, target, engine:, strategy:)` is the cross-object front-end for two docs
209
+ with NO shared ids:
210
+ 1. flatten each doc's data-fields; `engine.pair(source_fields, target_fields)` (genome+type+label+
211
+ options, ledger-first);
212
+ 2. for each ACCEPTED pair emit `:changed(label|type)` keyed by the **source** id; target-only field →
213
+ `:added`; a source with no accepted target AND not in the review set → `:removed`;
214
+ 3. **never guesses** — ambiguous + unmatched sources/targets are held in `#unresolved` (surfaced for a
215
+ human), NOT auto-paired / auto-added / auto-removed. A same-genome RELABEL is ambiguous by design
216
+ (could be a re-purpose) → not auto-emitted unless the ledger/human confirms the pair.
217
+
218
+ The emitted `[Change]` feeds the EXISTING `CommandSynthesizer` / `Deploy` unchanged (one emission
219
+ layer). `Deploy.from_cross_object(source, target, engine:, strategy:)` + `Deploy#pairing` expose the
220
+ accepted/ambiguous/unmatched for adjudication before apply.
221
+
222
+ ## 9. Unified BUILD emitter — `Builder::TemplateBuilder` (shares the emission layer)
223
+
224
+ `lib/ecoportal/api/graphql/builder/template_builder.rb`. Turns a declarative template spec
225
+ (stages→sections→fields→options→config→gauge-stops) into an ordered `WorkflowCommand` batch for
226
+ `Builder::Template#create/update(commands:)`. Order: addStage → addSection → addStageSection →
227
+ editSectionHeader → addField → addSelectFieldOption / addGaugeFieldStop / editFieldConfiguration(byType:).
228
+
229
+ It uses the SAME `placeholderId` id-threading primitive as `CommandSynthesizer`: every created node
230
+ gets a deterministic client-chosen `placeholderId` and later same-batch commands reference it (stage
231
+ placeholder ← addStageSection.stageId + addField.stageId; section placeholder ← addStageSection.sectionId
232
+ + addField.sectionId; field placeholder ← option/stop/config dataFieldId). So BUILD (add everything)
233
+ and DIFF-DEPLOY (add/remove/edit the delta) emit through ONE layer. Only VALID command-input keys are
234
+ emitted (input classes slice + compact); config is passed through under its byType sub-hash exactly as
235
+ supplied — nothing is fabricated. UNSUPPORTED in a build spec (deferred): forces/strategies/callbacks/
236
+ tasks/recipients (shape unconfirmed); `field_type` enum not schema-validated (passed through as given).
237
+
238
+ ## 10. Related
239
+
240
+ - `template_diff_pairing_domain.md` — the full problem space (READ FIRST).
241
+ - `lib/.../input/workflow_command.rb` — the command vocabulary both emitters target.
242
+ - `.ai-assistance/projects/template-automatic-build-maintenance/{INTENT,TODO}.md` (the merged plan;
243
+ `template-diff-deploy/` + `template-maintenance/` are now pointers).
@@ -89,16 +89,26 @@ this field in template Y", even after the page was created, cloned, or restored.
89
89
  had a full solution using the genome to pair fields during template changes and page
90
90
  restorations (backup to YAML + restore).
91
91
 
92
- **Current state:**
93
- - Mostly **unused** the Migrator solution was abandoned due to maintenance cost and
94
- low usage
95
- - **Not indexed** the genome is not sent to Elasticsearch
96
- - **Unpaired on many pages** tech scripts that added fields without copying from the
97
- template introduced fields with random creation-time genome signatures, breaking the
98
- pairing with the template
99
- - **NewEp port status:** Uncertain unclear whether the NewEp namespace fully ported
100
- the Genome functionality from the Enzyme namespace
101
- - The genome is NOT currently exploitable for scripting or integration purposes
92
+ **Current state (CORRECTED 2026-07-03 against the live schema — supersedes earlier claims):**
93
+ - **IT IS EXPOSED IN GraphQL.** The live NewEp schema exposes **`genomeSignature: String`** on the
94
+ **`DataFieldsInterface`** and on **every concrete data-field type** (PlainText, Select, CrossReference,
95
+ People, Date, Number, Gauge, Checklist, TagField, Geo, Signature, ContractorEntities, ImageGallery,
96
+ File, Mailbox, ActionsList, Law, RichText, AiSummary, TableField, Chart, FrequencyRateChart,
97
+ EmbeddedStructure, PageAction). Verified in `.ai-assistance/tmp/20260605T101224_live_ep_graphql_schema.graphql.json`.
98
+ APIv2 also exposed it. **This corrects the previous "NOT currently exploitable" claim, which was wrong.**
99
+ - It is on **data fields only** (per the introspection) not on sections/stages/pages.
100
+ - **Our gem does not yet REQUEST it** — no fragment/model field. To use it: add `genomeSignature` to the
101
+ data-field fragment + `Base::Page::DataField`. (This is the real gap, not schema availability.)
102
+ - **Not indexed** — the genome is not sent to Elasticsearch.
103
+ - **Unreliable as a sole key** — see the failure modes (field re-use/re-purpose/revamp keep the same
104
+ genome for a different meaning; wrong for select options; etc.). Treat as a **strong-but-fallible
105
+ signal** in equivalence matching, never an unarguable key.
106
+ - The Migrator solution (`app/services/migration/*`, `Migration::GenomeKindship`) was abandoned for
107
+ maintenance/coverage reasons, but its founding principle (data lives in data-fields) is valid.
108
+
109
+ **Full treatment:** see `.ai-assistance/code/template_diff_pairing_domain.md` — the authoritative
110
+ capture of the template diff/pairing/deploy problem space (lifecycle, failure modes, equivalence
111
+ reframe, diff modalities, layering, pipeline, Product context).
102
112
 
103
113
  **Why it matters for the future:** The genome was the intended foundation for:
104
114
  - Automatically applying template changes to existing pages
@@ -0,0 +1,175 @@
1
+ # Template Lifecycle, Diffing, Pairing & Deployment — Domain Knowledge
2
+
3
+ **Status:** authoritative domain capture (harvestable by ep-ai-standards). Sourced from Oscar
4
+ (platform maintainer) 2026-07-03, with schema/code facts verified inline. This is the single
5
+ reference for the template diff/deploy/sync problem space — do not re-derive it from scratch.
6
+
7
+ Related: `graphql_domain_knowledge.md` (§ genome), `.ai-assistance/projects/template-diff-deploy/`,
8
+ `.ai-assistance/projects/template-maintenance/`, `.ai-assistance/projects/qa-services-delivery/`.
9
+
10
+ ---
11
+
12
+ ## 1. Why this matters
13
+
14
+ Most quality work in Services Delivery revolves around **template changes** and the **tech-script
15
+ fixes** applied to a register's pages after a **failed QA on a published template** produced wrong
16
+ pages. Templates are the epicenter of admin + troubleshooting automation — **not a QA-only concern**.
17
+
18
+ ## 2. The UAT→PROD template lifecycle and its failure modes
19
+
20
+ Jira tickets (service **xor** support) raise actions to **change templates** (most commonly). Those
21
+ changes are either:
22
+ - applied **directly to the PROD template** (or a DEV template that becomes PROD on the org launch date), or
23
+ - applied first to the **UAT** site template, then replicated to PROD after customer approval.
24
+
25
+ Because the whole process is **manual**, it regresses in two directions:
26
+ - **Regression A (lost changes):** someone unpublishes/replaces the PROD template with a **YAML backup
27
+ copy of UAT**. Any PROD-only changes not present in UAT are **silently lost**.
28
+ - **Regression B (premature release):** UAT already carried changes **not yet approved for release**;
29
+ replacing PROD with UAT **releases them**, and the customer becomes unhappy.
30
+
31
+ Current mitigation (costly): hand-account **every** UAT change in detail; the **same staff member**
32
+ who changed UAT re-does those changes in PROD. This introduces its own errors — PROD may differ from
33
+ UAT, or the person forgets some changes. Goal: make this **smooth, effective, efficient** — no extra
34
+ comms, no back-and-forth, no trust loss, no rectifying tech-scripts.
35
+
36
+ ## 3. The core obstacle — no stable identity (MongoDB)
37
+
38
+ MongoDB assigns a **distinct ID to every object in the nested page hierarchy**, and IDs are **not
39
+ shared** across: template vs its child pages, page vs page, UAT template vs PROD template. Nothing
40
+ has the same ID as its counterpart anywhere.
41
+
42
+ Consequences seen platform-wide:
43
+ - Customer integrations must **fetch the page model first** just to learn sub-object IDs (e.g.
44
+ `dataField` ids) before building a patch payload.
45
+ - The **field ref** (a truncated sha of the label name) is used as a pseudo-key in analytics — it's
46
+ unstable; the **Field ID project** exists to add certainty here.
47
+
48
+ ## 4. THE key distinction — self-version diff vs cross-object diff
49
+
50
+ | | IDs | Pairing needed? | Difficulty |
51
+ |---|---|---|---|
52
+ | **Self-version** (same object, vN vs vN+1) | **retained** | none — match by id | trivial ✅ **built** |
53
+ | **Cross-object** (UAT↔PROD, page↔template) | **different** | yes — equivalence | hard |
54
+
55
+ - **Self-version** captures "what did this ticket change to the UAT template" exactly, as long as the
56
+ original snapshot was retained. Implemented: `Ecoportal::API::GraphQL::Diff::VersionDiff` (gem).
57
+ - **Cross-object** is the hard one: to diff, you must **pair counterpart objects** across two id-spaces,
58
+ translate to a common id-space in memory, diff, then map back. Only a fully-mapped copy can be diffed.
59
+
60
+ ## 5. The genome signature — VERIFIED facts + why it can't be the sole key
61
+
62
+ **What it is:** an identifier introduced by the original ecoPortal developer (~2015/16) intended to
63
+ pair objects across a template and its child pages, and across YAML backup→restore. The genome is
64
+ **inherited** template-(sub)object → child-page-(sub)object, and copied on restore. It **grows** each
65
+ time it is restored or a child page is created (a related backend commit had to be amended for this).
66
+
67
+ **⚠️ VERIFIED 2026-07-03 (corrects the older "not exposed" claim in `graphql_domain_knowledge.md`):**
68
+ The live NewEp GraphQL schema (`.ai-assistance/tmp/20260605T101224_live_ep_graphql_schema.graphql.json`)
69
+ **DOES expose `genomeSignature: String`** on the **`DataFieldsInterface`** and on **every concrete
70
+ data-field type** (PlainText, RichText, Date, Number, Gauge, People, Select, Checklist, TagField, Geo,
71
+ Signature, ContractorEntities, CrossReference, ImageGallery, File, Mailbox, ActionsList, Law,
72
+ AiSummary, TableField, Chart, FrequencyRateChart, EmbeddedStructure, PageAction). APIv2 also exposed it
73
+ (developer "Rien" eventually exposed everything). It is **on data fields, not on sections/stages/pages**
74
+ (per the introspection). **Our gem does NOT yet request it** — no fragment/model field — so to use it
75
+ we add `genomeSignature` to the data-field fragment + `Base::Page::DataField`. It is **not indexed** in
76
+ Elasticsearch.
77
+
78
+ **Why genome is NOT an unarguable pairing key (Oscar's failure modes — design constraints):**
79
+ - (a) pairing was **manual**, with no platform support;
80
+ - (b) **new features** (new field types, linked-field configs, …) were never added to the genome solution;
81
+ - (c) it's **wrong for select options** — an option's key is the **value (primary)** or **name
82
+ (auxiliary)**, never the genome;
83
+ - (d) it wrongly **syncs checklist fields** from the template (shouldn't happen);
84
+ - (e) the premise "genome is always an unarguable pairing key" is **false**: a configurator may
85
+ **re-use** a field for something else then add a same-name/type field (realising the mistake); a
86
+ structural/conceptual **revamp** may carry same-label/type fields re-used for a different purpose; or
87
+ a field is **re-purposed** keeping its genome (e.g. `Date logged:` → `Date of sign-off:`);
88
+ - (f) **data-loss accounting** is incomplete and under-documented.
89
+
90
+ The abandoned backend solution built on genome: `app/services/migration/*`, notably
91
+ `Migration::GenomeKindship` — synced children pages to their parent template via genome pairing.
92
+ Abandoned for the reasons above, but its **founding principle is valid** (see §6).
93
+
94
+ ## 6. The load-bearing simplifying principle (valid)
95
+
96
+ **Sections/stages are scaffolding, NOT data containers.** Customer data lives in the **data-fields**
97
+ (+ core page properties: old tags → now `location_ids` + `custom_tags`, `created_at`, …). Therefore
98
+ **restructuring is tractable as long as you can pair the customer data between the original-stage
99
+ field and the end-state field.** This dramatically shrinks the problem: **pair fields precisely; treat
100
+ structure as context.** That `genomeSignature` lives on data-fields aligns exactly with this.
101
+
102
+ Proven in practice by the migration-scripts framework (`doc/scripts/v*`): field-pairing via `FIELDS_MAP`
103
+ (`20231129_stolt_tra_migrate.rb`, `20240125_gns_con_migrate.rb`); later, due to page variability in
104
+ some registers, a different approach — **re-import via the current template's import spreadsheet**
105
+ (customer fills a data CSV extract) + a final tech-script for non-importable fields
106
+ (`20251202_luna_park_migration.rb`, `20260615_resene_copy_files.rb`).
107
+
108
+ ## 7. Pairing = equivalence matching (the reframe — CONFIRMED by Oscar 2026-07-03)
109
+
110
+ Stable identity across independent Mongo objects does not exist, so **reframe from identity to
111
+ EQUIVALENCE** — a **record-linkage / entity-resolution** problem:
112
+
113
+ - Combine **multiple weak signals** into a **confidence score**:
114
+ `genomeSignature` (strong, now available on data fields), **type + label** (already implemented:
115
+ eco-helpers `HelpersMigration::TypedFieldsPairing` / `copying.rb`), **section heading + position**,
116
+ **select options-by-value**, structural context.
117
+ - **Auto-accept** high confidence; **route ambiguous / low-confidence to a HUMAN**; the assistant asks
118
+ the user to resolve unmatched cases and adjudicate doubtful pairings.
119
+ - **Persist every decision to a LEARNING LEDGER** (confirmed a first-class artifact): the pairings +
120
+ the log of *how* each was resolved, for future reuse. Over time most cases auto-resolve; the tool
121
+ asks only about genuine novelty. The accumulated ledger also **feeds Product's Field-ID /
122
+ template-entity-id** effort (we are the interim bridge that generates their data).
123
+
124
+ ## 8. Diff MODALITIES — multiple diff forms for different processes (Oscar 2026-07-03)
125
+
126
+ There is **no single diff**. Different processes need different modes. Fields may **move between
127
+ sections**, or **must go to a specific section** — some processes care about section membership,
128
+ others only about the field's data pairing. The engine is a **family of strategies** over configurable
129
+ axes:
130
+
131
+ - **Pairing strategy:** by-id (self-version) · genome · type+label · assisted/ledger (cross-object).
132
+ - **Comparison scope:** structural (stages/sections/fields) · config-only (field configuration,
133
+ options, forces) · data-migration (field→field data pairing, structure-agnostic).
134
+ - **Move sensitivity:** section-move-aware vs section-agnostic; ordering-sensitive vs -insensitive.
135
+ - **Direction/intent:** changelog (what changed, for a ticket) · deployment delta (UAT→PROD replay) ·
136
+ sync-readiness (can this register/subset be synced to the active template?).
137
+
138
+ `Diff::VersionDiff` implements the **self-version, id-paired, structural, section-move-aware** mode.
139
+ The others compose the same `Change` output with a different pairing front-end and comparison scope.
140
+
141
+ ## 9. Layering — where each capability lives (CONFIRMED)
142
+
143
+ | Layer | Owns | Consumers |
144
+ |---|---|---|
145
+ | **ecoportal-api-graphql** (gem) | *mechanics*: read model, WorkflowCommand vocab, build (emitter) + **diff (VersionDiff)** + apply; `genomeSignature` fragment (to add) | everyone |
146
+ | **eco-helpers** | *orchestration*: pairing engine (human-in-loop), learning ledger, samples, session flows, `TypedFieldsPairing` | tools, scripts |
147
+ | **ecoportal-qa** | QA-facing surface (checks/reporters/Jira/CLI); **should use the gem's model, not re-implement it** | QA team |
148
+ | admin / troubleshooting / integrations | consume gem + eco-helpers directly | — |
149
+
150
+ ## 10. The pipeline (how the pieces compose)
151
+
152
+ ```
153
+ pair (id | genome | type+label | assisted+ledger)
154
+ └─► diff on paired model (mode-specific)
155
+ └─► delta as WorkflowCommands (the emitter)
156
+ └─► apply to target ─► verify (ecoportal-qa) ─► monitor (register sync-ready tickboxes)
157
+ ```
158
+ The **self-version changelog** is a portable "commit": a `Change` set that, once id-translated through
159
+ a pairing map, **replays onto PROD**.
160
+
161
+ ## 11. Product context
162
+
163
+ Register-template-sync is **not yet scoped** by Product (Field-ID takes precedence). Product is working
164
+ out a **template-entity-id** to differentiate template versions within a register (to pair pages made
165
+ from a previous template version with the current one) + **customer filters by template**. Our tools
166
+ are the interim bridge and **generate the pairing data** Product needs.
167
+
168
+ ## 12. Status & next steps
169
+
170
+ - ✅ **Done:** `Diff::VersionDiff` (self-version, exact, id-paired) — gem, 8 specs.
171
+ - **Next:** (1) add `genomeSignature` to the data-field fragment + `Base::Page::DataField`; (2) synthesise
172
+ WorkflowCommands from a `Change` set (delta → commands); (3) the **pairing engine** (equivalence,
173
+ multi-signal, human-assisted, ledger) in eco-helpers; (4) **diff modalities** as strategies;
174
+ (5) cross-template deploy (UAT→PROD replay); (6) monitoring / sync-readiness; (7) tighten `ecoportal-qa`
175
+ to use the gem's page-model instead of its own `TemplateModel`.
@@ -436,3 +436,31 @@ payload = Mutation::Page::ExecuteWorkflowCommands.new(client).query(
436
436
  raise "Commands failed: #{payload.body.inspect}" unless payload.success?
437
437
  puts "Applied #{cmds.size} commands. New patchVer: #{payload.patchVer}"
438
438
  ```
439
+
440
+ ---
441
+
442
+ ## Schema realities — input-key gotchas (verified 2026-07-04 against the live introspection dump)
443
+
444
+ These are the ways the command bus does **not** behave the way you'd expect. Verifying against the
445
+ schema dump (`.ai-assistance/tmp/*_live_ep_graphql_schema.graphql.json`) before assuming a key exists
446
+ saves a live round trip.
447
+
448
+ - **`addField` (`WorkflowAddFieldInput`) keys = EXACTLY** `placeholderId, fieldType, label, stageId`
449
+ (NON_NULL), `sectionId` (NON_NULL), `column`. It does **not** accept `description` or `required`.
450
+ - **A field's `description` is set via `editFieldConfiguration`** (`WorkflowEditFieldConfigurationInput`
451
+ has a top-level `description: String`), NOT via `addField`. To build a field *with* a description
452
+ (e.g. a CSV / hidden-field **identity token**), emit `addField` then a follow-up
453
+ `editFieldConfiguration(dataFieldId: <field>, description:)`. This is the seam `Diff::CommandSynthesizer`
454
+ and `Builder::TemplateBuilder` use (via `placeholderId`/`ref` when the field is created in the same batch).
455
+ - **`required` cannot be set through the command bus at all.** There is **no `required` input key
456
+ anywhere** — not on `WorkflowAddFieldInput`, not on `WorkflowEditFieldConfigurationInput` (top-level),
457
+ not on any of its **12 `byType` sub-inputs**. Treat field-`required` as **read-only / out-of-band**:
458
+ read it from the template, don't try to mutate it. (ecoportal-qa `:skip`s `required`-readiness for
459
+ this reason; `VersionDiff`'s `BYTYPE_CONFIG` omits it.)
460
+ - **Structural back-refs:** an added field's `addField` carries its parent `sectionId` + `stageId`;
461
+ an added **section** has no stage-id key, so wire it with a follow-up
462
+ `addStageSection(stageId:, sectionId:)`. `Diff::CommandSynthesizer` threads all of these via
463
+ `placeholderId`/`ref()` (parent placeholder when same-batch, else real id; omitted when unknown).
464
+
465
+ **Rule of thumb:** before adding a key to a `WorkflowCommand` builder, confirm it exists on that input
466
+ type in the schema dump. Absent keys are silently dropped (or rejected) — they do not error informatively.
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## Pending
4
4
 
5
+ - [ ] **⏰ CI: query-validation step (DUE by 2026-07-17 — within 2 weeks of 2026-07-03)** — wire
6
+ `tests/validate_queries.rb` into CI so the whole class of GraphQL selection bugs fails *before*
7
+ release, not in a client runner. **Use a FRESHLY-introspected schema** (do NOT commit the 6.3MB
8
+ dump — it goes stale); fail the build only on the two structural classes ("field must have
9
+ selections" / "selections can't be made on scalars"). Motivated by the 2026-07-03 v1.3.10 hotfix
10
+ (LocationStructure `updatedAt`/`createdAt`) — those queries had no spec and first broke in prod.
11
+ Also: fix the 3 flagged union findings — **TRIAGED 2026-07-03: all THREE are REAL bugs** (verified
12
+ by rendering the assembled docs + re-validating fixes; not harness/dump artifacts). Each selects
13
+ fields directly on a union (invalid GraphQL) — wrap in the members' shared interface:
14
+ - `Query::PageWithForces` (page_with_forces.rb:33-35): bare `forces` on `PageUnion` →
15
+ `... on BasePageInterface { forces { ...ForceFields } }`. ALSO `fragment ForceFields`
16
+ (fragment/force.rb:15,20) selects `id` on `DataFieldBinding`/`SectionBinding` which have NO `id`
17
+ (only name/reference/referenceId) → drop `id` (or use `referenceId`).
18
+ - `Query::PagesWorkflowCommands` (pages_workflow_commands.rb:33-37): bare `workflow` on `PageUnion`
19
+ → `... on BasePageInterface { workflow { ... } }`. (The "unused variable" errors were an
20
+ artifact — a validator cascade from the union error; no action on the vars.)
21
+ - `Query::RegisterPresetViews` (register_preset_views.rb:71): bare `{id key name weight}` on
22
+ `FieldConfigurationUnion` → `... on FieldConfigurationInterface { id key name weight }`.
23
+ None of these 3 queries have specs — add render/validation specs alongside the fixes. These are
24
+ latent (not the users-sync incident); PageWithForces is WIP. Ship as a follow-up patch (1.3.11).
25
+
5
26
  - [x] **Builder::Register** — `createRegister`, `updateRegister`, `destroyRegister` mutations.
6
27
  `Base::Register`, `Model::Register`, `Input::Register::Create/Update`, `Payload::Register`.
7
28
  `Builder::Register` with `create`, `update`, `destroy`, and `preset_view` sub-builder.