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
|
@@ -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`).
|
|
@@ -5,9 +5,14 @@ Legend: `[ ]` todo · `[~]` in progress · `[x]` done · `[!]` blocked (see Risk
|
|
|
5
5
|
Each phase ends with a gate. **Do not flip an old class name to delegate until that
|
|
6
6
|
case's parity result is recorded in DECISIONS.md.**
|
|
7
7
|
|
|
8
|
+
> **STATUS 2026-07-02:** Phase 0 inventory DONE → see `INVENTORY.md`. Scope narrowed by Oscar:
|
|
9
|
+
> **build the native GraphQL classes ONLY this cycle** — no delegation flip, no shim removal yet
|
|
10
|
+
> (those come "long after we know everything works on the native samples"). So Phases 1–2 (native
|
|
11
|
+
> classes + specs) are IN; the flip/removal steps stay `[ ]` until Oscar green-lights.
|
|
12
|
+
|
|
8
13
|
---
|
|
9
14
|
|
|
10
|
-
## Phase 0 — Inventory & Baseline (no code changes)
|
|
15
|
+
## Phase 0 — Inventory & Baseline (no code changes) ✅ DONE 2026-07-02 (see INVENTORY.md)
|
|
11
16
|
|
|
12
17
|
### 0a. Map the full ooze case inheritance/include tree
|
|
13
18
|
- [ ] Confirm the inheritance chain (verified 2026-06-30, re-confirm before building):
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Decisions — QA-as-CI/CD (ecoportal-qa)
|
|
2
|
+
|
|
3
|
+
Append per decision; never delete — history matters.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [2026-07-02] Standalone repo, governed by ep-ai-standards (not built inside it)
|
|
8
|
+
|
|
9
|
+
**Context:** Oscar was unsure whether ep-ai-standards tooling should live in its own repo or inside
|
|
10
|
+
ep-ai-standards, noting it "shouldn't matter as long as results don't clash and ep-ai-standards can
|
|
11
|
+
pick it up and ensure continuity."
|
|
12
|
+
|
|
13
|
+
**Decision:** **Standards & governance live in ep-ai-standards; implementations live where their
|
|
14
|
+
dependencies live.** `ecoportal-qa` depends on the GraphQL gem → its own repo. It carries a
|
|
15
|
+
`.ai-assistance/` **conformance declaration** (`conformance.json` + `CLAUDE.md`) so ep-ai-standards
|
|
16
|
+
can discover, index, and govern it without owning the code.
|
|
17
|
+
|
|
18
|
+
**Reason:** Makes "location doesn't matter" true *by construction*: continuity via the declaration,
|
|
19
|
+
no clash via shared conventions. Keeps the governance repo lean and each tool independently testable.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [2026-07-02] A QA "pseudo-language" (declarative DSL) → compiler → jobs
|
|
24
|
+
|
|
25
|
+
**Decision:** QA authors declarative **check-sets** in a small YAML DSL (front-end). A Ruby
|
|
26
|
+
**compiler** turns them into runnable Check objects (IR), and later into CI-job / auto-approve
|
|
27
|
+
backends.
|
|
28
|
+
|
|
29
|
+
**Reason:** Delegation surface for QA (no Ruby needed); front-end/back-end split insulates QA
|
|
30
|
+
templates from the volatile WorkflowCommand/GraphQL API. Proven pattern (Great Expectations,
|
|
31
|
+
Gherkin, policy-as-code, CI-as-code).
|
|
32
|
+
|
|
33
|
+
**Consequences:** The DSL schema is a public contract to keep stable; compiler backends can churn.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [2026-07-02] Read-only, canonical-JSON result, honest skips
|
|
38
|
+
|
|
39
|
+
**Decision:** The runner is **read-only** end to end. Every run emits one **canonical Result JSON**
|
|
40
|
+
(ticket · target · verdict · per-check outcome) that all reporting derives from and that is the audit
|
|
41
|
+
record. Checks that can't be evaluated (e.g. a `required` flag not captured in a snapshot) return
|
|
42
|
+
**`:skip`**, never a false pass; verdict distinguishes `passed` / `passed_with_gaps` / `changes_requested`.
|
|
43
|
+
|
|
44
|
+
**Reason:** Safety/privacy (no writes), governance (auditability), and trustworthy results (a skip is
|
|
45
|
+
not a pass) are the stated decision drivers.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## [2026-07-02] Jira phased; human-in-the-loop; auto-approve last & opt-in
|
|
50
|
+
|
|
51
|
+
**Decision:** Phase Jira integration: **read + post results + human-gated transition first**; the
|
|
52
|
+
**auto-approve hook is built last, opt-in, and QA-owned** (off by default). Jira is the
|
|
53
|
+
ticket-management workflow layer, not the check engine.
|
|
54
|
+
|
|
55
|
+
**Reason:** Services & Delivery is a human team; agents assist, not replace. Governance/human gate are
|
|
56
|
+
non-negotiable. Lowest-risk capability (read-only + report) delivers value before any transition logic.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## [2026-07-02] Ruby core, no runtime deps in the core
|
|
61
|
+
|
|
62
|
+
**Decision:** Core (DSL/compiler/runner/reporters) has **no runtime gem dependencies** — it operates
|
|
63
|
+
on a page/template doc Hash. `Source::GraphqlSource` lazily requires `ecoportal-api-graphql` only when
|
|
64
|
+
run against a live org.
|
|
65
|
+
|
|
66
|
+
**Reason:** Ruby for team resourcing redundancy; a dependency-free core stays offline-runnable and
|
|
67
|
+
trivially testable (specs run against a captured snapshot), and keeps CI fast.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## [2026-07-03] PROPOSED — Jira adapter: interface + REST first, MCP later (pending Oscar)
|
|
72
|
+
|
|
73
|
+
**Context:** Phase 3 posts results to Jira. Two ways in: the Atlassian MCP (no secrets, but may be
|
|
74
|
+
absent in headless/CI runs) vs Jira REST (needs a managed service-account token, but works everywhere).
|
|
75
|
+
|
|
76
|
+
**Proposed decision:** define a `Jira::Client` interface and implement **`RestClient` first**; add
|
|
77
|
+
`McpClient` later as an interactive convenience. The endgame is Phase 4 CI ("ticket as pipeline"),
|
|
78
|
+
where MCP likely isn't available — REST is the durable path and won't need redoing. The interface
|
|
79
|
+
makes it a drop-in choice, not a fork. **Not yet ratified** — awaiting Oscar's answers on the target
|
|
80
|
+
Jira project, workflow statuses, and whether a service-account token is available (see PHASE3-SCOPE.md).
|
|
81
|
+
|
|
82
|
+
**Reason:** avoid building a channel that can't run where the destination (CI) lives.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## [2026-07-03] Phase 3 does NOT auto-transition (reaffirms human-in-the-loop)
|
|
87
|
+
|
|
88
|
+
**Decision:** the Jira adapter's default is **report-only** (post a redacted, idempotent result
|
|
89
|
+
comment); a status transition happens **only** when a human explicitly names one (`--transition`).
|
|
90
|
+
Auto-approve stays out of scope until Phase 5 (opt-in + governed + audited).
|
|
91
|
+
|
|
92
|
+
**Reason:** the human gate is non-negotiable; posting a result is useful on its own and carries no
|
|
93
|
+
risk, so it ships first while transition automation waits for the governed Phase 5 design.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Project: QA-as-CI/CD for Services Delivery (ecoportal-qa)
|
|
2
|
+
|
|
3
|
+
**Created:** 2026-07-02
|
|
4
|
+
**Status:** active — Phase 1 prototype built
|
|
5
|
+
**Primary repo:** `ecoportal-qa` (`C:\ruby_scripts\git\ecoportal-qa`) — NEW standalone Ruby tool
|
|
6
|
+
**Reads via:** `ecoportal-api-graphql` (the GraphQL client gem)
|
|
7
|
+
**Governed by:** `ep-ai-standards` (conformance declared in `ecoportal-qa/.ai-assistance/`)
|
|
8
|
+
|
|
9
|
+
> **Why the plan lives here:** `ecoportal-qa` is the implementation repo; the team's project-cycle
|
|
10
|
+
> docs live in `ecoportal-api-graphql/.ai-assistance/projects/` (same pattern as the ooze migration).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Problem
|
|
15
|
+
|
|
16
|
+
The QA team in **Services Delivery** reviews **org configuration & template changes** against Jira
|
|
17
|
+
tickets before sign-off. Today this is manual and slow. They want to (a) quickly see *what has been
|
|
18
|
+
done*, (b) author repeatable **automated checks** that save review time, and (c) eventually gate
|
|
19
|
+
Jira ticket transitions on those checks — with **humans always in the loop** (this is a Services &
|
|
20
|
+
Delivery team; agents/AI assist, they don't replace people).
|
|
21
|
+
|
|
22
|
+
## Vision — "QA-as-CI/CD"
|
|
23
|
+
|
|
24
|
+
QA authors checks once, in a friendly declarative language; a Ruby compiler turns them into runnable
|
|
25
|
+
jobs; results flow to reporting + Jira; humans gate the transitions.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
QA template (YAML DSL) → Compiler (Ruby) → runnable Checks → Runner (read-only) → Result (JSON)
|
|
29
|
+
▲ what QA writes ▲ front-end→IR ▲ vs live/captured template ▲ reporting · Jira · monitoring
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Classic compiler split: QA owns a **stable front-end language**; the compiler emits **volatile
|
|
33
|
+
back-end job code**. That insulation matters — the underlying WorkflowCommand/GraphQL API is volatile,
|
|
34
|
+
and QA templates must not break when it shifts.
|
|
35
|
+
|
|
36
|
+
## Decision drivers (Oscar)
|
|
37
|
+
|
|
38
|
+
safety · privacy · simplicity · **delegation to the QA team** · monitoring · reporting · governance ·
|
|
39
|
+
Ruby (resourcing redundancy) · human-in-the-loop. Jira is *where the ticket-management workflow is
|
|
40
|
+
handled*, not where the engine runs.
|
|
41
|
+
|
|
42
|
+
## Scope
|
|
43
|
+
|
|
44
|
+
### In scope
|
|
45
|
+
- The **QA DSL** (declarative check-set: `meta.ticket/target` + `expect.stages→sections→fields`).
|
|
46
|
+
- A **compiler** (DSL → ordered, stateless Check objects / IR).
|
|
47
|
+
- A **runner** that verifies checks **read-only** against a live or captured template.
|
|
48
|
+
- A **canonical Result** (JSON) — the single source of truth for all reporting + the audit record.
|
|
49
|
+
- Sources: `FixtureSource` (offline JSON snapshot) + `GraphqlSource` (live, read-only, via the gem).
|
|
50
|
+
- Reporters: JSON (canonical) + Text now; CSV/HTML next.
|
|
51
|
+
- **ep-ai-standards conformance** stub so the tool is discoverable/governed without being owned.
|
|
52
|
+
|
|
53
|
+
### Out of scope (non-goals / later)
|
|
54
|
+
- **Writing** to orgs from the check path (read-only by design — safety/privacy).
|
|
55
|
+
- **Auto-approval** without a human — only ever an opt-in, QA-owned hook, built last.
|
|
56
|
+
- Building QA templates for every customer (that's QA's ongoing authoring, enabled by this tool).
|
|
57
|
+
- Replacing human QA judgement.
|
|
58
|
+
|
|
59
|
+
## Subject of review (confirmed)
|
|
60
|
+
**Org config & template changes** — templates/registers/workflows/fields/forces — sourced from the
|
|
61
|
+
GraphQL API. (A template IS a page, so structure reads via the page model.)
|
|
62
|
+
|
|
63
|
+
## Precedents (this pattern is proven)
|
|
64
|
+
- **Great Expectations** — declarative "expectations" → validation engine → results + data docs
|
|
65
|
+
(closest 1:1 analogy; "config/template expectations").
|
|
66
|
+
- **Gherkin/Cucumber** — a QA-writable language compiled to executable steps.
|
|
67
|
+
- **OPA / policy-as-code** — declarative rules → decisions + audit (governance model).
|
|
68
|
+
- **GitLab/GitHub CI-as-code** — YAML pipelines → jobs (the "ticket as pipeline" backend target).
|
|
69
|
+
|
|
70
|
+
## Success criteria
|
|
71
|
+
1. QA can author a check-set in YAML and get a green/red result against a real template with **zero
|
|
72
|
+
Ruby**. *(Phase 1 — DONE against the toocs snapshot.)*
|
|
73
|
+
2. Results are canonical JSON, auditable (ticket · target · verdict · per-check), and human-readable.
|
|
74
|
+
3. Read-only end to end; no PII leaks in results/logs.
|
|
75
|
+
4. Jira transitions are human-gated; auto-approve is opt-in and off by default.
|
|
76
|
+
5. The tool is discoverable/governed by ep-ai-standards via its conformance declaration.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Phase 3 — Jira adapter (human-gated) — SCOPE
|
|
2
|
+
|
|
3
|
+
**Status:** SCOPED (not built) — 2026-07-03. Building is blocked on two Oscar decisions (below).
|
|
4
|
+
**Repo:** `ecoportal-qa`. **Depends on:** Phase 1–2 (Runner + canonical Result + Reporters + Redactor).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Connect the QA runner to **Jira** so a QA run is: (1) **traceable** to a ticket, (2) its result is
|
|
11
|
+
**posted back** to that ticket, and (3) a **human gates** any status change. Phase 3 does **not**
|
|
12
|
+
auto-transition — that is Phase 5, opt-in and governed. Jira is the *ticket-management workflow layer*;
|
|
13
|
+
the check engine stays exactly as built.
|
|
14
|
+
|
|
15
|
+
Non-goal (Phase 3): automatic transitions, running on a Jira webhook (that's Phase 4 CI), editing
|
|
16
|
+
ticket content beyond a result comment + an optional verdict field.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## The one idea that keeps this simple
|
|
21
|
+
|
|
22
|
+
Everything already funnels into **one canonical `Result`** (ticket · target · verdict · per-check).
|
|
23
|
+
The Jira adapter is **just another consumer of that Result** — exactly like the CSV/HTML reporters.
|
|
24
|
+
It never reaches into the runner or the checks. So Phase 3 adds a *channel*, not a *rewrite*.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Runner ─► Result ─┬─► Reporters (text/json/csv/html) [Phase 1–2]
|
|
28
|
+
└─► Jira::Publisher ─► Jira::Client ─► a ticket [Phase 3] ← new channel only
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
### Traceability (ticket ↔ check-set)
|
|
36
|
+
- The link is the check-set's `meta.ticket` (e.g. `SD-1234`). **Push model:** you run a check-set,
|
|
37
|
+
the adapter posts to the ticket named in its `meta`.
|
|
38
|
+
- A reverse index (ticket → check-set file) is derivable by scanning `qa/checks/` — deferred until we
|
|
39
|
+
know where check-sets live long-term (Open Q #4).
|
|
40
|
+
|
|
41
|
+
### Components (all new, in `ecoportal-qa`)
|
|
42
|
+
- **`Jira::Client`** — a thin interface: `find_issue(key)`, `add_comment(key, body)`,
|
|
43
|
+
`set_field(key, field, value)`, `available_transitions(key)`, `transition(key, name)`.
|
|
44
|
+
- **`Jira::RestClient`** — real implementation over Jira Cloud REST v3 (base URL + service-account
|
|
45
|
+
API token from ENV/secret store). *Recommended first implementation* (see decision below).
|
|
46
|
+
- **`Jira::McpClient`** — optional implementation over the Atlassian MCP (no secrets in our code).
|
|
47
|
+
- **`Jira::DryRunClient`** — records intended calls, performs none. The offline test double + the safe
|
|
48
|
+
default for `--dry-run`. (Same pattern as `Source::FixtureSource`.)
|
|
49
|
+
- **`Jira::Publisher`** — orchestrates: render the result (TextReporter for the comment, JSON/HTML as
|
|
50
|
+
attachment), **redact first** (Redactor), post the comment, optionally set a verdict field, and —
|
|
51
|
+
only when a human explicitly asks — offer/perform a transition.
|
|
52
|
+
|
|
53
|
+
### Human-in-the-loop transition model (the safety crux)
|
|
54
|
+
Phase 3 supports three modes, defaulting to the safest:
|
|
55
|
+
1. **`--dry-run`** (default in CI-less/interactive) — print what *would* be posted; touch nothing.
|
|
56
|
+
2. **report-only** — post the result comment (+ optional verdict field); **leave the transition to
|
|
57
|
+
the human in the Jira UI.** This is the recommended day-one mode.
|
|
58
|
+
3. **`--transition "<name>"`** — a person runs this explicitly after reading the result; the tool
|
|
59
|
+
performs that one named transition. The tool NEVER picks/performs a transition on its own.
|
|
60
|
+
|
|
61
|
+
### Governance / safety
|
|
62
|
+
- **Redact before posting** (comments can be widely visible) — reuse the Phase-2 Redactor.
|
|
63
|
+
- **Secrets**: REST token from ENV/secret store, scoped to comment + transition on the target project
|
|
64
|
+
only; never committed. MCP path carries no secrets in our code.
|
|
65
|
+
- **Idempotency**: tag the comment with a hidden marker (`<!-- ecoportal-qa:run SD-1234 -->`) so a
|
|
66
|
+
re-run *updates* its previous comment instead of spamming the ticket.
|
|
67
|
+
- **Audit**: every Jira call appended to a run log (issue · action · verdict · timestamp · actor).
|
|
68
|
+
- **Least action**: the default does not transition; transitions are explicit, named, human-invoked.
|
|
69
|
+
|
|
70
|
+
### CLI surface (extends Phase 2)
|
|
71
|
+
```
|
|
72
|
+
ecoportal-qa report CHECKSET.qa.yml --source fixture:PATH|graphql:ID \
|
|
73
|
+
--jira [--jira-client rest|mcp|dry-run] [--transition "QA Approved"] [--redact] [--attach json,html]
|
|
74
|
+
```
|
|
75
|
+
- No `--jira` → behaves exactly like Phase 2 `run` (prints a report).
|
|
76
|
+
- `--jira` without `--transition` → report-only (comment + optional field).
|
|
77
|
+
- Exit code still encodes the verdict (CI-friendly).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Decision needed: Atlassian MCP vs Jira REST
|
|
82
|
+
|
|
83
|
+
| | Atlassian MCP | Jira REST API |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| Secrets in our code | none (session auth) | service-account token (managed) |
|
|
86
|
+
| Works in CI / headless | **uncertain** — interactive-auth MCP servers may be absent in headless/cron | **yes** |
|
|
87
|
+
| Setup speed (interactive) | fast | moderate |
|
|
88
|
+
| Control / stability | good, but tied to MCP availability | full, documented |
|
|
89
|
+
|
|
90
|
+
**Recommendation:** build the **`Jira::Client` interface + `RestClient` first**. Reason: the endgame
|
|
91
|
+
is Phase 4 CI ("ticket as pipeline"), where MCP likely isn't available; REST works everywhere, so it's
|
|
92
|
+
the durable path and doesn't have to be redone. Add `McpClient` later as an interactive convenience.
|
|
93
|
+
The interface makes this a drop-in choice, not a fork.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Sub-phases (build order once unblocked)
|
|
98
|
+
- **3a** — `Jira::Client` interface + `Jira::DryRunClient` + `Jira::Publisher` (format/redact/idempotency).
|
|
99
|
+
**Fully offline-specable** (DryRunClient records calls) — same as Phase 1–2, no live Jira needed.
|
|
100
|
+
- **3b** — `Jira::RestClient` (config: base URL, token, project) + integration behind an env-gated spec.
|
|
101
|
+
- **3c** — CLI `report --jira …` wiring + audit log.
|
|
102
|
+
- **3d** — (optional) `Jira::McpClient`.
|
|
103
|
+
|
|
104
|
+
**Gate 3:** a QA run posts a redacted, idempotent result comment to a real ticket; transitions only
|
|
105
|
+
happen when a human names one. No auto-transition anywhere.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Open questions (Oscar) — these block the build
|
|
110
|
+
1. **Which Jira project + workflow statuses?** Need the exact transition names QA uses
|
|
111
|
+
(e.g. `In QA → QA Approved` / `Changes Requested`).
|
|
112
|
+
2. **REST service-account + API token**, or start **MCP-only interactive**? (Drives 3b vs 3d order.)
|
|
113
|
+
3. **Verdict field**: a dedicated custom field, or comment-only for now?
|
|
114
|
+
4. **Where do check-sets live** long-term (repo `qa/checks/`, per-customer, shared config repo)? —
|
|
115
|
+
affects the reverse ticket→check-set index (deferred until answered).
|