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.
- checksums.yaml +4 -4
- data/.ai-assistance/code/diff_pairing_engine.md +243 -0
- data/.ai-assistance/code/graphql_domain_knowledge.md +20 -10
- data/.ai-assistance/code/template_diff_pairing_domain.md +175 -0
- data/.ai-assistance/code/workflow-command-guide.md +28 -0
- data/.ai-assistance/projects/ooze-graphql-native-migration/INVENTORY.md +136 -0
- data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +6 -1
- data/.ai-assistance/projects/qa-services-delivery/DECISIONS.md +93 -0
- data/.ai-assistance/projects/qa-services-delivery/INTENT.md +76 -0
- data/.ai-assistance/projects/qa-services-delivery/PHASE3-SCOPE.md +115 -0
- data/.ai-assistance/projects/qa-services-delivery/ROADMAP.md +99 -0
- data/.ai-assistance/projects/qa-services-delivery/TODO.md +81 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +77 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +97 -0
- data/.ai-assistance/projects/template-diff-deploy/INTENT.md +12 -0
- data/.ai-assistance/projects/template-diff-deploy/TODO.md +9 -0
- data/.ai-assistance/projects/template-maintenance/PHASE0-FINDINGS.md +93 -0
- data/.ai-assistance/projects/template-maintenance/README.md +14 -0
- data/CHANGELOG.md +87 -0
- data/docs/worklog.md +279 -0
- data/ecoportal-api-graphql.gemspec +1 -1
- data/lib/ecoportal/api/graphql/base/page/data_field.rb +1 -1
- data/lib/ecoportal/api/graphql/builder/template_builder.rb +174 -0
- data/lib/ecoportal/api/graphql/builder.rb +17 -16
- data/lib/ecoportal/api/graphql/diff/change.rb +59 -0
- data/lib/ecoportal/api/graphql/diff/command_synthesizer.rb +329 -0
- data/lib/ecoportal/api/graphql/diff/cross_object_diff.rb +165 -0
- data/lib/ecoportal/api/graphql/diff/deploy.rb +121 -0
- data/lib/ecoportal/api/graphql/diff/id_resolver.rb +64 -0
- data/lib/ecoportal/api/graphql/diff/pairing/candidate.rb +32 -0
- data/lib/ecoportal/api/graphql/diff/pairing/engine.rb +173 -0
- data/lib/ecoportal/api/graphql/diff/pairing/ledger.rb +119 -0
- data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +104 -0
- data/lib/ecoportal/api/graphql/diff/strategy.rb +113 -0
- data/lib/ecoportal/api/graphql/diff/version_diff.rb +332 -0
- data/lib/ecoportal/api/graphql/diff.rb +34 -0
- data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +1 -0
- data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +27 -18
- data/lib/ecoportal/api/graphql/mutation/action/archive.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/action/create.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/action/update.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/contractor_entity/create.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/contractor_entity/destroy.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/contractor_entity/update.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +1 -1
- data/lib/ecoportal/api/graphql.rb +1 -0
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- data/tests/dump_template_model.rb +90 -0
- data/tests/validate_queries.rb +31 -9
- metadata +31 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78e2adbcdb62b6d2fdbf2ddb5906d9d8b5b93464af6269936b7eeda2dc94cb17
|
|
4
|
+
data.tar.gz: e6c55777dab78795abe7b81f419275cb52ec82a986aa92ee0da4affa4d5ca7e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
-
|
|
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`).
|