ecoportal-api-graphql 1.3.10 → 1.3.12
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/filter_contract_matrix.md +177 -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 +87 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +108 -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/.ai-assistance/skills/procedural-memory/SKILL.md +319 -0
- data/.ai-assistance/standards-version.json +21 -20
- data/CHANGELOG.md +119 -0
- data/CLAUDE.md +10 -0
- data/docs/self-docs/ARCHITECTURE.md +88 -0
- data/docs/self-docs/CHANGES.jsonl +7 -0
- data/docs/self-docs/CONVENTIONS.md +74 -0
- data/docs/self-docs/INTEGRATIONS.md +63 -0
- data/docs/self-docs/OVERVIEW.md +51 -0
- data/docs/self-docs/STATUS.md +69 -0
- data/docs/self-docs/self-docs-index.json +39 -0
- data/docs/worklog.md +256 -0
- data/ecoportal-api-graphql.gemspec +1 -1
- data/lib/ecoportal/api/common/graphql/model/diffable.rb +54 -54
- data/lib/ecoportal/api/graphql/base/action.rb +43 -43
- data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +67 -67
- data/lib/ecoportal/api/graphql/base/page/data_field/collection.rb +7 -0
- data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +28 -28
- data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +25 -25
- data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +24 -24
- data/lib/ecoportal/api/graphql/base/page/data_field.rb +1 -1
- data/lib/ecoportal/api/graphql/base/page/section_collection.rb +85 -79
- data/lib/ecoportal/api/graphql/base/preset_view.rb +17 -17
- data/lib/ecoportal/api/graphql/base/register.rb +18 -18
- 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/compat/filter_translator.rb +63 -28
- data/lib/ecoportal/api/graphql/concerns/page_compat.rb +51 -51
- data/lib/ecoportal/api/graphql/concerns.rb +14 -14
- 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/file_upload/client.rb +181 -181
- data/lib/ecoportal/api/graphql/fragment/page.rb +85 -85
- data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +1 -0
- data/lib/ecoportal/api/graphql/input/action/update.rb +14 -14
- data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +41 -41
- data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +47 -47
- data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +49 -49
- data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +27 -27
- data/lib/ecoportal/api/graphql/input/search_conf.rb +436 -367
- data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +27 -18
- data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +30 -30
- data/lib/ecoportal/api/graphql/logic/input.rb +26 -26
- 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 +40 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74b442ca224440bee94f3f88522a4233b995d187db1b70ba31a0801e0691d3eb
|
|
4
|
+
data.tar.gz: 5d5d05fdf46fac5a32a12c5d4c8e47c21b913332bbe04c40a3ab81beeacbab9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d667e7ed09fc6ed6fd200695567eb25a8bb5f4f41b3b98882e04d84acd1775c1a17dc7d424e82f26b7f89d4f826bb46c25fddc0ef5fc526e8731aaccd73193b
|
|
7
|
+
data.tar.gz: 324b54e96de9f7ae82e71bf1b2c0427fa6fd4ed99d0153ec836a85cf68d76d5ce8f8462865b29cddd446015caa2b4a5fcaf940c5c2866970d7814a2d0495a73b
|
|
@@ -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).
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Filter Contract Matrix — GraphQL register/page filters
|
|
2
|
+
|
|
3
|
+
**Purpose.** Evidence-based ground-truth mapping of *ES register/page filter operations* to
|
|
4
|
+
their exact params, per register field type, so the compat `FilterTranslator` can be validated
|
|
5
|
+
against the backend rather than conjecture. Built after the Date/Select translator bug
|
|
6
|
+
(commit e45ba58) where the original translation table was never validated.
|
|
7
|
+
|
|
8
|
+
**Authoritative source.** `ecoPortal_master` Rails backend, `NewEp::Es` filter stack:
|
|
9
|
+
|
|
10
|
+
- **Field-type → allowed operations** (the contract a register exposes):
|
|
11
|
+
`app/services/new_ep/es/definitions/page.rb`
|
|
12
|
+
- **Operation → exact params** (constructor kwargs / `argument` declarations):
|
|
13
|
+
`app/services/new_ep/es/filters/pages/**` and `.../filters/global/**`
|
|
14
|
+
- **Operation string → class dispatch:**
|
|
15
|
+
`app/services/new_ep/es/filters/deserializer.rb:51-58` — the `operation` string is
|
|
16
|
+
`camelize`d + suffixed `Filter`, resolved **first** under `NewEp::Es::Filters::Pages::`,
|
|
17
|
+
then falling back to `NewEp::Es::Filters::Global::`. An operation with no matching class
|
|
18
|
+
raises `UnknownFilterError` (`deserializer.rb:59`). This is why `options_filter` /
|
|
19
|
+
`text_filter` (v2 names) are NOT valid GraphQL operations — see §"v2 → GraphQL" below.
|
|
20
|
+
|
|
21
|
+
All line numbers below are `file:line` in `ecoPortal_master` unless noted. GraphQL param names
|
|
22
|
+
match the backend constructor / `argument` declarations exactly.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. v2 → GraphQL translator input types (what `Compat::FilterTranslator` handles)
|
|
27
|
+
|
|
28
|
+
`lib/ecoportal/api/graphql/compat/filter_translator.rb` accepts *v2-style* `type:` hashes and
|
|
29
|
+
emits GraphQL `{operation:, params:}` hashes. The v2 `type` names are NOT backend operation
|
|
30
|
+
names; the emitted `operation` names ARE. Every emitted operation below resolves to a real
|
|
31
|
+
backend class (verified via the deserializer dispatch).
|
|
32
|
+
|
|
33
|
+
| v2 `type` | Emitted GraphQL `operation` | Emitted params | Backend class (evidence) | Translator handles? |
|
|
34
|
+
|---|---|---|---|---|
|
|
35
|
+
| `date_filter` (system key, e.g. `created_at`) | `date_filter` | `key, time_zone, gte?(=lbound), lte?(=ubound)` | `Global::DateFilter` — `date_filter.rb:28` (`key, path, gt, gte, lt, lte`) | YES (`filter_translator.rb:110-124`) |
|
|
36
|
+
| `date_filter` (field key, e.g. `date.zXXXX`) | `range_date_filter` | `key, path=membranes.date, time_zone, gte?, lte?` | `Pages::RangeDateFilter` — `range_date_filter.rb:797` (`key, path, gte, lte, gt, lt`) | YES (`filter_translator.rb:110-124`) |
|
|
37
|
+
| `text_filter` | `match_filter` | `key, value(=query)` | `Global::MatchFilter` — `match_filter.rb:6` (`key, value, path`) | YES (`filter_translator.rb:74-82`) |
|
|
38
|
+
| `exact_filter` / `is_filter` | `is_filter` | `key, path?(derived membranes.<type>), value` | `Pages::IsFilter` — `is_filter.rb:258` (`key, path, value`) | YES (`filter_translator.rb:52-57`) |
|
|
39
|
+
| `options_filter` (system key) | `one_of_filter` / `none_of_filter` (exclude) | `key, values(=chosen)` | `Global::OneOfFilter` — `one_of_filter.rb:16` (`key, values`); `Global::NoneOfFilter` — `none_of_filter.rb:6` (`key, values`) | YES (`filter_translator.rb:90-104`) |
|
|
40
|
+
| `options_filter` (field key, e.g. `select_str.zXXXX`) | `has_any_filter` / `doesnt_have_any_filter` (exclude) | `key, path(=membranes.<type>), field_key='selected', values(=chosen)` | `Pages::HasAnyFilter` — `has_any_filter.rb:995` → `Pages::BaseHasAnyFilter` — `base_has_any_filter.rb:10` (`key, path, field_key, values, invert`); `Pages::DoesntHaveAnyFilter` — `doesnt_have_any_filter.rb:1061` | YES (`filter_translator.rb:90-104`) |
|
|
41
|
+
| `tag_filter` | *(passthrough — no rewrite)* | — | n/a (already GraphQL-shaped) | passthrough (`filter_translator.rb:37-39`) |
|
|
42
|
+
| `register_filter` | *(passthrough)* | — | `Pages::RegisterFilter` (`filters/pages/register_filter.rb`) | passthrough |
|
|
43
|
+
| `and_filter` / `or_filter` | *(passthrough)* | — | `Global::AndFilter` / `Global::OrFilter` | passthrough |
|
|
44
|
+
| already-`{operation:,params:}` hash | *(passthrough)* | — | — | passthrough (unmatched `type`) |
|
|
45
|
+
| SearchConf value object (`to_h`, not Hash) | *(passthrough)* | — | — | passthrough (`filter_translator.rb:25`) |
|
|
46
|
+
|
|
47
|
+
### Why `options_filter` / `text_filter` are NOT emitted verbatim
|
|
48
|
+
|
|
49
|
+
- **`options_filter`**: there is NO `Pages::OptionsFilter` or `Global::OptionsFilter` class.
|
|
50
|
+
The closest backend class is `Pages::OptionsFieldFilter` (op `options_field_filter`,
|
|
51
|
+
`options_field_filter.rb:9`, takes `chosen`/`mode`) — a *different* operation the register
|
|
52
|
+
definitions never expose. Emitting `options_filter` verbatim would raise `UnknownFilterError`.
|
|
53
|
+
So the translator rewrites to the backend-blessed `has_any_filter`/`one_of_filter`.
|
|
54
|
+
- **`text_filter`**: a `Pages::TextFilter` class DOES exist (`text_filter.rb:1252`) but its
|
|
55
|
+
constructor takes `key:, query:` (not `value:`) and register definitions never expose a
|
|
56
|
+
`text_filter` operation — they expose `contains_filter`/`is_filter`. The translator therefore
|
|
57
|
+
emits `match_filter` (`Global::MatchFilter`, `match_filter.rb:6`), which is a valid operation
|
|
58
|
+
with `{key, value}`. NOTE: `match_filter` matches the *analyzed* value (partial/full-text);
|
|
59
|
+
for whole-value exact use `is_filter` (see §exact-vs-partial in `search_filters.md`).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. Field-type contract matrix (register/page fields)
|
|
64
|
+
|
|
65
|
+
One row per **(field type, operation)** enumerated from `page.rb`. `key` = field definition key
|
|
66
|
+
(`<type>.zXXXX`); `path` = the nested membranes path the definition hardcodes. `field_key` /
|
|
67
|
+
`values` shown where the operation carries a subfield. "Translator?" marks whether the compat
|
|
68
|
+
`FilterTranslator` currently produces this operation from a v2 input.
|
|
69
|
+
|
|
70
|
+
### 2.1 System / metadata fields (no `membranes.*` path)
|
|
71
|
+
|
|
72
|
+
| Field | v2 type | GraphQL operation(s) | params | page.rb evidence | Translator? |
|
|
73
|
+
|---|---|---|---|---|---|
|
|
74
|
+
| Created on / Updated on | `date_filter` | `date_filter`, `isnt_in_date_filter` | `key=created_at`, gte/lte/time_zone | `page.rb:31-39` | YES (system date branch) |
|
|
75
|
+
| External ID | `exact_filter` | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter` | `key=external_id`, value | `page.rb:65-70` | YES via `exact_filter`→`is_filter` (no path — system key) |
|
|
76
|
+
| Page ID (`mould_counter`) | — | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `contains_filter`, `doesnt_contain_filter` | `key=mould_counter`, value | `page.rb:72-79` | is_filter YES; contains_filter NO |
|
|
77
|
+
| Page name (`name`) | `text_filter`/`exact_filter` | `is_filter`, `isnt_filter`, `contains_filter`, `doesnt_contain_filter` | `key=name`, value | `page.rb:81-86` | is_filter YES; match/contains partial: `text_filter`→`match_filter` (valid but `name` uses QueryFilter internally — see NOTE) |
|
|
78
|
+
| Page status (`state`) | `options_filter`/`exact` | `is_filter`, `isnt_filter` | `key=state`, value(`@extras`) | `page.rb:87-99` | via `exact_filter`→`is_filter` YES |
|
|
79
|
+
| System ID (`id`) | `exact_filter` | `is_filter`, `isnt_filter`, `one_of_filter` | `key=id`, value / values | `page.rb:101-105` | is_filter + one_of_filter YES |
|
|
80
|
+
| Assigned to | — | `is_filter`, `isnt_filter`, `has_all/any_filter`, `doesnt_have_all/any_filter` | `key=assigned_to`, field_key=`assigned_to`, values | `page.rb:15-22` | NO (no v2 driver) |
|
|
81
|
+
| Created by | — | `is_filter`, `isnt_filter`, `has_any_filter`, `doesnt_have_any_filter` | `key=creator_id`, field_key=`creator_id` | `page.rb:24-29` | NO |
|
|
82
|
+
| Created via (`source`) | — | `is_filter`, `isnt_filter` | `key=source`, value | `page.rb:41-45` | via is_filter YES (if driven) |
|
|
83
|
+
| Custom tags (`other_tags`) | `tag_filter` | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter` | `key=other_tags`, field_key=`other_tags`, values | `page.rb:56-63` | passthrough (`tag_filter`) |
|
|
84
|
+
| Tags / locations (`all_location_ids`) | `tag_filter` | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter` | `key=all_location_ids`, field_key=`all_location_ids`, values | `page.rb:107-114` | passthrough (values lowercased backend-side: `has_any_filter.rb:1024`) |
|
|
85
|
+
| Current Stage (`active_stage_names`) | — | `has_any/all_filter`, `doesnt_have_any/all_filter` | `key=active_stage_names`, field_key=`active_stage_names`, values | `page.rb:138-143` | NO |
|
|
86
|
+
|
|
87
|
+
NOTE (`name`): `contains_filter` on `name` routes to `Global::MatchFilter` (`contains_filter.rb:453-455`)
|
|
88
|
+
and `is_filter` on `name` routes to `Global::ExactFilter` on `name.exact` (`is_filter.rb:283-284`).
|
|
89
|
+
The translator's `text_filter`→`match_filter{key:'name',value:}` therefore hits the right backend
|
|
90
|
+
class for partial name search.
|
|
91
|
+
|
|
92
|
+
### 2.2 Register data fields (nested `membranes.<type>` path)
|
|
93
|
+
|
|
94
|
+
| Field type | GraphQL operation(s) | params (key/path/field_key/values) | page.rb evidence | Backend op class | Translator? |
|
|
95
|
+
|---|---|---|---|---|---|
|
|
96
|
+
| **date** (`date.zXXXX`) | `range_date_filter`, `isnt_in_range_date_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.date`; range: gte/lte/gt/lt/time_zone | `page.rb:191-209` | `Pages::RangeDateFilter` `range_date_filter.rb:797`; field_key resolved to `value` `range_date_filter.rb:822-828` | YES (`date_filter` field branch → `range_date_filter`) |
|
|
97
|
+
| **select_str** (`select_str.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.select_str`, field_key=`selected`, values | `page.rb:279-300` | `Pages::HasAnyFilter`→`BaseHasAnyFilter` `base_has_any_filter.rb:10`; search_key=`path.selected` `base_has_any_filter.rb:42-44` | YES (`options_filter` field branch → `has_any_filter`) |
|
|
98
|
+
| **select_num** (`select_num.zXXXX`) | same as select_str, path=`membranes.select_num` | key, path, field_key=`selected`, values | `page.rb:256-277` | `Pages::HasAnyFilter`→`BaseHasAnyFilter` | YES (field branch; caller passes path/`select_num` prefix) |
|
|
99
|
+
| **plain_text** (`plain_text.zXXXX`) | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `contains_filter`, `doesnt_contain_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.plain_text`; is_filter → `.exact` subfield | `page.rb:145-165` | `is_filter`→`Pages::IsFilter`→`BaseIsFilter` search_key=`exact` `is_filter.rb:315-316`; contains→`Pages::TextFilter` `contains_filter.rb:459-461` | is_filter YES (exact_filter→is_filter); contains NO |
|
|
100
|
+
| **rich_text** (`rich_text.zXXXX`) | `contains_filter`, `doesnt_contain_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.rich_text`; contains → `content` subfield | `page.rb:303-321` | `contains_filter`→`BaseContainsFilter` field_key=`content` `contains_filter.rb:477-478` | NO (no is/exact op; contains only) |
|
|
101
|
+
| **number** (`number.zXXXX`) | `numeric_range_filter`, `isnt_in_numeric_range_filter`, `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.number`; range: gte/lte/gt/lt on `.value` | `page.rb:168-188` | `numeric_range_filter`→`Pages::NumericRangeFilter` search_key=`path.value` `numeric_range_filter.rb:941-943` | is_filter YES; numeric_range NO |
|
|
102
|
+
| **gauge** (`gauge.zXXXX`) | `is_filter`, `isnt_filter`, `numeric_range_filter`, `isnt_in_numeric_range_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.gauge` | `page.rb:234-254` | numeric via `NumericRangeFilter`; is via `IsFilter` search_key=`value` `is_filter.rb:317-318` | is_filter YES; numeric_range NO |
|
|
103
|
+
| **file** (`file.zXXXX`) | `has_any_filter`, `doesnt_have_any_filter` (×`file_container_id`, `content_type`), `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.file`, field_key=`file_container_id`\|`content_type`, values | `page.rb:212-232` | `has_any_filter`→`Pages::File::HasAnyFilter` `has_any_filter.rb:1034-1035` | NO |
|
|
104
|
+
| **image_gallery** (`image_gallery.zXXXX`) | `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.image_gallery` | `page.rb:545-561` | `is_empty`→`Pages::ImageGallery::IsEmptyFilter` `is_empty_filter.rb:572-573` | NO |
|
|
105
|
+
| **people** (`people.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter` (×`id`/`contractor_organization_id`/`schema_id`/`prefilter_location_ids`), `is_direct_supervisor_filter`, `has_direct_supervisor_filter`, `is/isnt_contractor_administrator_filter`, `people_field_contains_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.people`, field_key varies, values | `page.rb:466-499` | `Pages::People::HasAnyFilter` `has_any_filter.rb:1030-1031` | NO |
|
|
106
|
+
| **contractor_entities** (`contractor_entities.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.contractor_entities`, field_key=`contractor_entities_ids`, values | `page.rb:324-344` | `BaseHasAnyFilter`; `required_data` loads ContractorEntity `base_has_any_filter.rb:27-28` | NO |
|
|
107
|
+
| **cross_reference / page_reference** (`cross_reference.zXXXX`, `any_reference_field`) | `has_any_filter`, `doesnt_have_any_filter`, `has_all_filter`, `doesnt_have_all_filter`, `meets_conditions_filter`(if CRR), `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.cross_references`, field_key=`id`, values=`@page_id`; meets_conditions: `register_id` | `page.rb:502-542` | `Pages::CrossReferences::HasAnyFilter` `has_any_filter.rb:1021-1022` | NO |
|
|
108
|
+
| **actions_list** (`actions_list.zXXXX`) | `has_any/all_filter`, `doesnt_have_any/all_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.actions_list`, field_key=`id`, values=`@action_id` | `page.rb:347-367` | `Pages::ActionsList::HasAnyFilter` `has_any_filter.rb:1032-1033` | NO |
|
|
109
|
+
| **checklist** (`checklist.zXXXX`) | `all_options_ticked_filter`, `not_all_options_ticked_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.checklist` | `page.rb:445-463` | `all_options_ticked_filter` = `Pages::AllOptionsTickedFilter` (`all_options_ticked_filter.rb`) | NO |
|
|
110
|
+
| **geo** (`geo.zXXXX`) | `is_filter`, `isnt_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.geo`; is → `address.exact` subfield | `page.rb:370-388` | `IsFilter` search_key=`address.exact` `is_filter.rb:319-320` | is_filter YES (if driven); rest NO |
|
|
111
|
+
| **signature** (`signature.zXXXX`) | `is_filter`, `isnt_filter` (value=`@user_id`), `range_date_filter`, `isnt_in_range_date_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.signature`; is → `signed_by_id`; range → `date` subfield | `page.rb:391-411` | `IsFilter` search_key=`signed_by_id` `is_filter.rb:323-324`; range → `Pages::Signature::RangeDateFilter` `range_date_filter.rb:810-811` | is_filter/range partial (translator emits generic ops; subfield resolution is backend-side by key prefix) |
|
|
112
|
+
| **mailbox** (`mailbox.zXXXX`) | `range_date_filter`, `isnt_in_range_date_filter`, `has_any/all_filter`, `doesnt_have_any/all_filter` (×`all_recipients`/`sender_email`), `contains_filter`, `doesnt_contain_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.mailbox`, field_key varies, values | `page.rb:414-442` | `Pages::Mailbox::*` `has_any_filter.rb:1019-1020` | NO |
|
|
113
|
+
| **law** (`law.zXXXX`) | `has_any_filter`, `doesnt_have_any_filter`, `is_empty_filter`, `isnt_empty_filter`, `presence_filter`, `not_presence_filter` | key, path=`membranes.law`, field_key=`snippets_ids`, values=`@law_snippet_id` | `page.rb:564-582` | `BaseHasAnyFilter`; `required_data` loads Snippet `base_has_any_filter.rb:29` | NO |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 3. Operation → param contract (backend constructor signatures)
|
|
118
|
+
|
|
119
|
+
Cited so the guard spec asserts exact param shapes. All under
|
|
120
|
+
`app/services/new_ep/es/filters/`.
|
|
121
|
+
|
|
122
|
+
| Operation | Namespace / class | Constructor params (evidence) |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `date_filter` | `Global::DateFilter` | `key, path?, gt?, gte?, lt?, lte?` — `global/date_filter.rb:28` |
|
|
125
|
+
| `range_date_filter` | `Pages::RangeDateFilter` | `key, path?, gte?, lte?, gt?, lt?` — `pages/range_date_filter.rb:797` |
|
|
126
|
+
| `is_filter` | `Pages::IsFilter` | `key, path?, value?` — `pages/is_filter.rb:258` |
|
|
127
|
+
| `isnt_filter` | `Pages::IsntFilter` | `key, path?, value` — `pages/isnt_filter.rb:360` |
|
|
128
|
+
| `match_filter` | `Global::MatchFilter` | `key, value, path?` — `global/match_filter.rb:6` |
|
|
129
|
+
| `contains_filter` | `Pages::ContainsFilter` | `key, path?, value` — `pages/contains_filter.rb:445` |
|
|
130
|
+
| `has_any_filter` | `Pages::HasAnyFilter` → `BaseHasAnyFilter` | `key, path?, field_key?, values` — `pages/has_any_filter.rb:995`, `pages/base_has_any_filter.rb:10` |
|
|
131
|
+
| `has_all_filter` | `Pages::HasAllFilter` → `BaseHasAllFilter` | `key, path?, field_key?, values` — `pages/has_all_filter.rb:1135`, `pages/base_has_all_filter.rb:59` |
|
|
132
|
+
| `doesnt_have_any_filter` | `Pages::DoesntHaveAnyFilter` | `key, path?, field_key?, values` (invert) — `pages/doesnt_have_any_filter.rb:1061` |
|
|
133
|
+
| `doesnt_have_all_filter` | `Pages::DoesntHaveAllFilter` | `key, path?, field_key?, values` (invert) — `pages/doesnt_have_all_filter.rb:1199` |
|
|
134
|
+
| `one_of_filter` | `Global::OneOfFilter` | `key, values` — `global/one_of_filter.rb:16` |
|
|
135
|
+
| `none_of_filter` | `Global::NoneOfFilter` | `key, values` — `global/none_of_filter.rb:6` |
|
|
136
|
+
| `numeric_range_filter` | `Pages::NumericRangeFilter` | `key, path?, gte?, lte?, gt?, lt?` — `pages/numeric_range_filter.rb:917` |
|
|
137
|
+
| `is_empty_filter` | `Pages::IsEmptyFilter` | `key, path?` (search_key by type) — `pages/is_empty_filter.rb:552` |
|
|
138
|
+
| `presence_filter` | `Pages::PresenceFilter` | `key, path?` — `pages/presence_filter.rb:724` |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 4. Known-UNCOVERED field types / operations (visible gap, not silent)
|
|
143
|
+
|
|
144
|
+
The compat `FilterTranslator` intentionally translates only the v2 filter shapes that real
|
|
145
|
+
consumers emit (`date_filter`, `text_filter`, `exact_filter`/`is_filter`, `options_filter`,
|
|
146
|
+
plus passthroughs). The following register field types / operations have a backend contract
|
|
147
|
+
(cataloged above) but NO translator branch. This is by design — do not add speculative
|
|
148
|
+
translations without a consumer. The guard spec enumerates these so the gap stays visible.
|
|
149
|
+
|
|
150
|
+
**Field types with NO dedicated translator path** (any v2 filter targeting them falls through
|
|
151
|
+
to passthrough — the caller must pass a pre-shaped `{operation:,params:}` hash):
|
|
152
|
+
|
|
153
|
+
- `number` (numeric_range_filter), `gauge` (numeric_range_filter)
|
|
154
|
+
- `rich_text` (contains_filter / doesnt_contain_filter)
|
|
155
|
+
- `file` (has_any_filter on file_container_id / content_type)
|
|
156
|
+
- `image_gallery` (is_empty / presence only)
|
|
157
|
+
- `people`, `contractor_entities`, `cross_reference` / `page_reference`
|
|
158
|
+
- `actions_list`, `checklist`, `mailbox`, `law`, `signature` (range/is subfields)
|
|
159
|
+
- `geo` (is_filter on address.exact — reachable via exact_filter if the caller sets key `geo.zXXXX`)
|
|
160
|
+
|
|
161
|
+
**System fields with NO translator driver:** `assigned_to`, `creator_id` (Created by),
|
|
162
|
+
`active_stage_names` (Current Stage), `mould_counter` contains_filter.
|
|
163
|
+
|
|
164
|
+
**Operations never emitted by the translator** (must be passed pre-shaped): `has_all_filter`,
|
|
165
|
+
`doesnt_have_all_filter`, `numeric_range_filter`, `isnt_in_numeric_range_filter`,
|
|
166
|
+
`isnt_in_range_date_filter`, `contains_filter`, `doesnt_contain_filter`, `is_empty_filter`,
|
|
167
|
+
`isnt_empty_filter`, `presence_filter`, `not_presence_filter`, `meets_conditions_filter`,
|
|
168
|
+
`all_options_ticked_filter`, `people_field_contains_filter`, direct-supervisor / contractor-admin
|
|
169
|
+
filters.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 5. Change log
|
|
174
|
+
|
|
175
|
+
- 2026-07-08 — Initial matrix built from `page.rb` + `filters/pages/**` + `filters/global/**`.
|
|
176
|
+
Guard spec `spec/ecoportal/api/graphql/compat/filter_translator_spec.rb` extended to pin every
|
|
177
|
+
emitted operation to its backend param contract and to enumerate the uncovered types.
|
|
@@ -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
|