ecoportal-api-graphql 1.3.10 → 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 (52) 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/ooze-graphql-native-migration/INVENTORY.md +136 -0
  7. data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +6 -1
  8. data/.ai-assistance/projects/qa-services-delivery/DECISIONS.md +93 -0
  9. data/.ai-assistance/projects/qa-services-delivery/INTENT.md +76 -0
  10. data/.ai-assistance/projects/qa-services-delivery/PHASE3-SCOPE.md +115 -0
  11. data/.ai-assistance/projects/qa-services-delivery/ROADMAP.md +99 -0
  12. data/.ai-assistance/projects/qa-services-delivery/TODO.md +81 -0
  13. data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +77 -0
  14. data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +97 -0
  15. data/.ai-assistance/projects/template-diff-deploy/INTENT.md +12 -0
  16. data/.ai-assistance/projects/template-diff-deploy/TODO.md +9 -0
  17. data/.ai-assistance/projects/template-maintenance/PHASE0-FINDINGS.md +93 -0
  18. data/.ai-assistance/projects/template-maintenance/README.md +14 -0
  19. data/CHANGELOG.md +87 -0
  20. data/docs/worklog.md +279 -0
  21. data/ecoportal-api-graphql.gemspec +1 -1
  22. data/lib/ecoportal/api/graphql/base/page/data_field.rb +1 -1
  23. data/lib/ecoportal/api/graphql/builder/template_builder.rb +174 -0
  24. data/lib/ecoportal/api/graphql/builder.rb +17 -16
  25. data/lib/ecoportal/api/graphql/diff/change.rb +59 -0
  26. data/lib/ecoportal/api/graphql/diff/command_synthesizer.rb +329 -0
  27. data/lib/ecoportal/api/graphql/diff/cross_object_diff.rb +165 -0
  28. data/lib/ecoportal/api/graphql/diff/deploy.rb +121 -0
  29. data/lib/ecoportal/api/graphql/diff/id_resolver.rb +64 -0
  30. data/lib/ecoportal/api/graphql/diff/pairing/candidate.rb +32 -0
  31. data/lib/ecoportal/api/graphql/diff/pairing/engine.rb +173 -0
  32. data/lib/ecoportal/api/graphql/diff/pairing/ledger.rb +119 -0
  33. data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +104 -0
  34. data/lib/ecoportal/api/graphql/diff/strategy.rb +113 -0
  35. data/lib/ecoportal/api/graphql/diff/version_diff.rb +332 -0
  36. data/lib/ecoportal/api/graphql/diff.rb +34 -0
  37. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +1 -0
  38. data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +27 -18
  39. data/lib/ecoportal/api/graphql/mutation/action/archive.rb +1 -1
  40. data/lib/ecoportal/api/graphql/mutation/action/create.rb +1 -1
  41. data/lib/ecoportal/api/graphql/mutation/action/update.rb +1 -1
  42. data/lib/ecoportal/api/graphql/mutation/contractor_entity/create.rb +1 -1
  43. data/lib/ecoportal/api/graphql/mutation/contractor_entity/destroy.rb +1 -1
  44. data/lib/ecoportal/api/graphql/mutation/contractor_entity/update.rb +1 -1
  45. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +1 -1
  46. data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +1 -1
  47. data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +1 -1
  48. data/lib/ecoportal/api/graphql.rb +1 -0
  49. data/lib/ecoportal/api/graphql_version.rb +1 -1
  50. data/tests/dump_template_model.rb +90 -0
  51. data/tests/validate_queries.rb +31 -9
  52. metadata +31 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 180d0e0ea70c7fb3462ae6bdec6ef78d1480c97be0861bec6e3e4bbb4abba751
4
- data.tar.gz: e2733075993edee570ef5762fe06cdaac035609e313f583b43753a4da21f0fd3
3
+ metadata.gz: 78e2adbcdb62b6d2fdbf2ddb5906d9d8b5b93464af6269936b7eeda2dc94cb17
4
+ data.tar.gz: e6c55777dab78795abe7b81f419275cb52ec82a986aa92ee0da4affa4d5ca7e8
5
5
  SHA512:
6
- metadata.gz: e05ae227d2cc97f91c250b74c6e831cbec0f9c9f8822d0bfdebacccfbe519cf229a48f2b9222623faf1c1ff2622ac2e7c1538e0036fa4494cb7a3e9082ba6f51
7
- data.tar.gz: ed6a82e38512fee2375a0bd880c1c721cee1d28597055cbeda9117a454d59f7d0a6b8bfdb1cb43fbe66fa18860a7508f426fb982ac0035d01b26140e789b1ecd
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.
@@ -0,0 +1,136 @@
1
+ # INVENTORY — Ooze → Native GraphQL Migration (Phase 0)
2
+
3
+ Verified 2026-07-02 against actual code in `eco-helpers` (main code), `ecoportal-api-graphql`
4
+ (gem), and `implementation/training` (downstream consumers). This is the **master schedule
5
+ partition**: which cases can migrate now vs. are blocked on forces, and the BC surface that must
6
+ keep working with **zero customer-script edits**.
7
+
8
+ > **Scope note (Oscar, 2026-07-02):** this project currently builds the **native GraphQL classes
9
+ > only**. We do NOT yet flip the `OozeSamples::*` names to delegate, and we do NOT remove shim
10
+ > code. The delegation flip happens "long after we know everything works on the native samples."
11
+
12
+ ---
13
+
14
+ ## A. The ooze case inheritance tree (eco-helpers)
15
+
16
+ Root path: `lib/eco/api/usecases/ooze_samples/`
17
+
18
+ ```
19
+ Eco::API::Common::Loaders::UseCase
20
+ ├─ OozeSamples::OozeBaseCase (ooze_base_case.rb) include Helpers, Helpers::Rescuable
21
+ │ └─ OozeSamples::OozeRunBaseCase (ooze_run_base_case.rb) single-ooze runner
22
+ │ └─ OozeSamples::OozeUpdateCase (ooze_update_case.rb)
23
+ │ └─ OozeSamples::OozeFromDocCase (ooze_from_doc_case.rb) + Word-doc parsing
24
+ │ └─ OozeSamples::RegisterUpdateCase (register_update_case.rb) include Helpers::Creatable ← CUTOVER #1
25
+ │ └─ OozeSamples::TargetOozesUpdateCase (target_oozes_update_case.rb) CSV target ids ← CUTOVER #2
26
+ │ └─ OozeSamples::RegisterMigrationCase (register_migration_case.rb) include HelpersMigration
27
+ └─ OozeSamples::RegisterExportCase (register_export_case.rb) standalone (NOT under OozeBaseCase)
28
+ └─ OozeCases::ExportRegisterCase (ooze_cases/export_register_case.rb) include ExportableRegister
29
+ ```
30
+
31
+ ### Control-flow loop a native class must reproduce (RegisterUpdateCase)
32
+
33
+ - `main(session, options, usecase, mode: :legacy)` → init KPIs → `:legacy` runs
34
+ `with_each_entry { process_ooze(&block) }`; `:delegate` yields for a custom loop → log KPIs.
35
+ - `with_each_entry` → `batched_search_results` yields page batches → per page: dedup check →
36
+ `ooze(id)` load → yield to block → `update_oozes` (batch save).
37
+ - `batched_search_results` → `apiv2.registers.search(register_id, search_options) { ... }`,
38
+ batch-size chunking.
39
+ - `search_options` → sort=created_at asc, `conf_search` (→ `search` hook), `conf_filters`
40
+ (→ `filters` hook).
41
+ - KPI counters: `total_search_oozes`, `retrieved_oozes`, `dupped_search_oozes`, `updated_oozes`,
42
+ `failed_update_oozes`, `created_oozes`, `ooze_create_attempts`.
43
+ - Dry-run: `results_preview` (confirm total, 10s timeout); `display_patch` / `backup_patch!` in base.
44
+ - Internal batch queue: `enqueue` / `queue_shift` / `queue_get` (duck-types v2 Page / PageStage /
45
+ anything responding to `dirty?` + `as_update`).
46
+
47
+ `TargetOozesUpdateCase` overrides only `with_each_entry` (reads target ids from CSV column 1+,
48
+ batches by size) — everything else inherited.
49
+
50
+ **Native counterpart already has a head start:** eco-helpers
51
+ `lib/eco/api/usecases/graphql/samples/pages/page/base.rb` already implements a **native GraphQL
52
+ loop** (`process` → `init_kpis` → `each_page` → `process_page` → `log_kpis`, plus `update_page`
53
+ with a simulate guard and register-scoped `search_conf`). The native register case builds on this
54
+ `Pages::Page::Base`, NOT on a v2 loop.
55
+
56
+ ---
57
+
58
+ ## B. Helper modules — v2-coupled vs pure
59
+
60
+ Root: `lib/eco/api/usecases/ooze_samples/helpers/` and `helpers_migration/`
61
+
62
+ | Module | Path | Classification | Migration action |
63
+ |---|---|---|---|
64
+ | `Helpers::Shortcuts` | helpers/shortcuts.rb | **PURE** (string simplify/compare; one `obj.force` in a message) | Port ~verbatim to `graphql/helpers/pages/` |
65
+ | `Helpers::Filters` | helpers/filters.rb | **PURE** (tag/date filters, time math) | Port ~verbatim |
66
+ | `Helpers::Rescuable` | helpers/rescuable.rb | error-rescue wrapper | Port (thin) |
67
+ | `Helpers::Creatable` | helpers/creatable.rb | **v2-coupled** (`apiv2.pages.get_new/create`; duck-types v2 Page) | GraphQL re-expression (native draft + create) |
68
+ | `Helpers::OozeHandlers` | helpers/ooze_handlers.rb | **v2-coupled** (`merge_values` case-matches `V2::Page::Component::*`) | GraphQL-typed re-expression (per data-field type) |
69
+ | `Helpers::ExportableOoze` | helpers/exportable_ooze.rb | v2-coupled (field value reads) | later (export cases) |
70
+ | `Helpers::ExportableRegister` | helpers/exportable_register.rb | v2-coupled (ooze value aggregation) | later (export cases) |
71
+ | `HelpersMigration::Copying` | helpers_migration/copying.rb | **v2-coupled** (field pairing/copy) | later (migration case) |
72
+ | `HelpersMigration::TypedFieldsPairing` | helpers_migration/typed_fields_pairing.rb | **v2-coupled** (type+label pairing) | later (migration case) |
73
+
74
+ **First-wave port:** `Shortcuts`, `Filters`, `Rescuable` (pure/thin) + a native `Creatable`.
75
+
76
+ ---
77
+
78
+ ## C. The compat shim being retired (NOT this session)
79
+
80
+ `lib/eco/api/usecases/graphql/compat/ooze_redirect.rb` (+ `ooze_redirect/{dirty_array,field_patches,force_compat}.rb`).
81
+
82
+ - `OozeRedirect.included(base)` → `FieldPatches.apply!` (global v2-class reopening + GraphQL field
83
+ prepends), `base.include(GraphQLEnv)`, `base.prepend(Infrastructure)`,
84
+ `+ ForceCompat::Infrastructure if force_support?` (currently `false`).
85
+ - `Infrastructure` overrides (the leaf ops a native class implements directly instead):
86
+ `api_v2`/`apiv2` → `graphql`; `stage(name)` → `StageView`; `with_fields(ooz, type:, label:)` →
87
+ reads `ooz.components`; `dirty?` → `.dirty?` or captured submit/sign-off flags.
88
+ - `FieldPatches` steps: v2 `===` dispatch lie; GraphQL BasePage `submit!`/`sign_off!` + pending
89
+ flags; People `DirtyArray`; CrossReference `reference_ids`/`add`/`clear`; Select
90
+ `options`/`select`/`deselect`/`values`.
91
+
92
+ A native class needs NONE of these patches — it dispatches on GraphQL types directly.
93
+
94
+ ---
95
+
96
+ ## D. Forces partition (blocked until GraphQL forces endpoint is live)
97
+
98
+ - **Base ooze cases (A) do NOT use forces** — `OozeBaseCase`, `RegisterUpdateCase`,
99
+ `TargetOozesUpdateCase`, `RegisterMigrationCase`, `RegisterExportCase` are all force-free.
100
+ → **All first-wave cutover cases are migratable now.**
101
+ - **Forces live only in downstream training scripts** that subclass the register cases:
102
+ | Script | Base | Force usage |
103
+ |---|---|---|
104
+ | `les_mills_force_update_case.rb` (Test_Les) | RegisterUpdateCase | `forces.get_by_name` → `force.custom_script =` |
105
+ | `20231031_fix_options_case.rb` (FIXOPTIONS) | RegisterUpdateCase | `forces.each` → `force.custom_script =` |
106
+ | `08102024_THL_RemoveLocTitle_case.rb` (REMOVELOCTITLEHAZ) | RegisterUpdateCase | `forces.get_by_name` → `custom_script =` |
107
+ | `rename_field_case.rb` (AddFieldBinding) | RegisterUpdateCase | `forces.delete!` |
108
+ | `attach_image_case.rb` (AttachImage) | TargetOozesUpdateCase | `forces.get_by_name` |
109
+ - **Note:** the gem *does* ship forces write commands via the command bus
110
+ (`addForce`/`editForce`/`addBinding`/…) and `OozeRedirect.force_support?` being `false` is
111
+ stale/conservative (see memory `forces-via-workflow-commands`). But the ooze-case force *read*
112
+ path (`target.forces.get_by_name`, `force.custom_script=`) is a different surface and its
113
+ GraphQL end-to-end readiness is UNCONFIRMED. These scripts stay on the v2 path until confirmed.
114
+
115
+ ---
116
+
117
+ ## E. Downstream include-site inventory (BC surface)
118
+
119
+ - Training repo `C:\ruby_scripts\implementation\training` **exists**; **273+ custom cases**.
120
+ - **All are still APIv2-only** — none currently `include GraphQL::Compat::OozeRedirect` (the live
121
+ TOOCS/CANS cutover scripts that DO use it are not in this training tree — locate them before any
122
+ flip; not needed this session since we are not flipping).
123
+ - Overrides seen across consumers (the hooks a native class must keep honoring):
124
+ `process_ooze` (always), `search`, `filters`, `custom_processing`, plus script-local helpers
125
+ (`with_target_stage`, `target_stage`, `with_target_field`, `with_target_force`, etc.).
126
+
127
+ ---
128
+
129
+ ## F. Migration order (schedule)
130
+
131
+ 1. **Now (native only):** port pure helpers (`Shortcuts`, `Filters`, `Rescuable`) + native
132
+ `Creatable`; native register base case on top of `Pages::Page::Base`; specs. **No flip.**
133
+ 2. Native `RegisterUpdateCase` + `TargetOozesUpdateCase` counterparts; A/B parity harness.
134
+ 3. (Later) flip old names → delegate, per-case, parity-gated; remove shim slices.
135
+ 4. (Blocked) force-using cases — pending GraphQL force read/write end-to-end confirmation.
136
+ 5. (Later) export + migration cases (`ExportableRegister`, `Copying`, `TypedFieldsPairing`).