ecoportal-api-graphql 1.3.11 → 1.3.13
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/filter_contract_matrix.md +177 -0
- data/.ai-assistance/projects/compat-layer-audit/COMPAT_AUDIT.md +244 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +10 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +11 -0
- data/.ai-assistance/projects/template-model-logic/CATEGORY_CATALOG.md +236 -0
- data/.ai-assistance/projects/template-model-logic/CLASSIFICATION_SPIKE.md +243 -0
- data/.ai-assistance/projects/template-model-logic/DESIGN_NOTE.md +154 -0
- data/.ai-assistance/scripts/bridge-inbox-check.sh +75 -0
- data/.ai-assistance/skills/procedural-memory/SKILL.md +319 -0
- data/.ai-assistance/skills/project-self-docs/SKILL.md +181 -0
- data/.ai-assistance/skills/project-self-docs/scripts/self_docs_scan.py +378 -0
- data/.ai-assistance/standards-version.json +22 -21
- data/.claude/settings.json +150 -146
- data/.gitlab-ci.yml +45 -0
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +11 -0
- data/docs/self-docs/ARCHITECTURE.md +88 -0
- data/docs/self-docs/CHANGES.jsonl +12 -0
- data/docs/self-docs/COMPLIANCE.md +79 -0
- data/docs/self-docs/CONVENTIONS.md +74 -0
- data/docs/self-docs/INTEGRATIONS.md +65 -0
- data/docs/self-docs/OPERATIONS.md +76 -0
- data/docs/self-docs/OVERVIEW.md +64 -0
- data/docs/self-docs/STATUS.md +73 -0
- data/docs/self-docs/self-docs-index.json +51 -0
- data/docs/worklog.md +15 -20
- data/lib/ecoportal/api/common/graphql/client.rb +2 -0
- data/lib/ecoportal/api/common/graphql/http_client.rb +189 -177
- 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/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/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/file_upload/client.rb +181 -181
- data/lib/ecoportal/api/graphql/fragment/page.rb +85 -85
- 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/interface/location_structure/command.rb +30 -30
- data/lib/ecoportal/api/graphql/logic/input.rb +26 -26
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- metadata +20 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Category Catalog — Composite Question Archetypes (FIRST DRAFT)
|
|
2
|
+
|
|
3
|
+
> Companion to `DESIGN_NOTE.md`. This is the **closed first set** of concrete "category"
|
|
4
|
+
> archetypes for ecoPortal templates. A category is a reusable composite question pattern —
|
|
5
|
+
> a **parametrizable macro** authored once that compiles down into flat `fields + forces`.
|
|
6
|
+
> Authoring-first (per DESIGN_NOTE §5b): every archetype here is expressible via the
|
|
7
|
+
> shipped `WorkflowCommandInput` command bus write path.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## How archetypes compile to `fields + forces`
|
|
12
|
+
|
|
13
|
+
An archetype is a macro. Given its parameters (labels, options, which effect), it emits an
|
|
14
|
+
**ordered batch of `WorkflowCommand` inputs** — the same batch `Builder::TemplateBuilder`
|
|
15
|
+
already produces. The expansion has two halves:
|
|
16
|
+
|
|
17
|
+
1. **Structure** → `addField` (per dependent field, with `fieldType`/`label`/`stageId`/`sectionId`/`column`),
|
|
18
|
+
`addSelectFieldOption` (per option), `addGaugeFieldStop` (per gauge band),
|
|
19
|
+
`addSection`/`addStageSection` (when the archetype owns a whole revealable section),
|
|
20
|
+
and follow-up `editFieldConfiguration` for `description` (the identity/CSV token),
|
|
21
|
+
`tooltip`, and per-type `byType` config. Field structure is placeholder-threaded
|
|
22
|
+
(`placeholderId`) so intra-batch references resolve before the server assigns real ids.
|
|
23
|
+
2. **Flow** → one or more **forces** bound to the dependent fields. A force is an AngularJS/Lisp
|
|
24
|
+
snippet (`addForce {name, customScript}`) plus `addBinding {forceId, fieldId, name}` per field
|
|
25
|
+
it reads or writes. The force encodes the answer→effect mapping: `show`/`hide`/`disable`/`clear`,
|
|
26
|
+
`select`/`set`, `add-tag`/`remove-tag`, `accent` (color), and matrix lookups. In the
|
|
27
|
+
**EPTIR flat IR** target this same flow is modelled declaratively as a JSON-Logic `when`
|
|
28
|
+
condition + a closed effect (`show_field`/`hide_field`/`show_section`/`set_required`/`set_value`/
|
|
29
|
+
`add_tag`), which is then lowered to a force snippet at compile time.
|
|
30
|
+
|
|
31
|
+
> **Hard constraint (VERIFIED, `workflow-command-guide.md` §"Schema realities", `add_field.rb`):**
|
|
32
|
+
> `required` cannot be set through the command bus **anywhere** — not on `addField`, not on
|
|
33
|
+
> `editFieldConfiguration` top-level, not on any `byType` sub-input. So any archetype effect that
|
|
34
|
+
> "makes a field required" MUST be realised as **force-level conditional-required** (the force
|
|
35
|
+
> refuses to let the stage submit / flags the field), NOT as a static template `required` flag.
|
|
36
|
+
> Every `required_when_shown` below means *force-enforced*, not *template-attribute*.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Summary table
|
|
41
|
+
|
|
42
|
+
| # | Name | Parent question | Core effect | Grounding |
|
|
43
|
+
|---|------|-----------------|-------------|-----------|
|
|
44
|
+
| 1 | `conditional-detail-select` | Select (single) | reveal + require detail field on chosen answer(s) | **Well-grounded** (DESIGN_NOTE worked example; `numeric_behaviour_select`, `Bay_change_options`) |
|
|
45
|
+
| 2 | `yes-no-with-evidence` | Select (Yes/No) or Checklist | reveal File/ImageGallery (+ note) on "Yes" | **Well-grounded** (File/Image field types heavily used; special case of #1) |
|
|
46
|
+
| 3 | `checklist-with-followup` | Checklist (multi) | each unchecked/"No" item reveals its own follow-up field | **Plausible** (Checklist type real; per-item force wiring inferred) |
|
|
47
|
+
| 4 | `multi-select-fanout` | Select (multiple) | each selected option enables/disables its own section | **Well-grounded** (`replace_risk` `update-unit` per-`category` section enable/disable) |
|
|
48
|
+
| 5 | `risk-matrix` | 2× Select (likelihood, consequence) | compute rating via lookup table → Gauge + level Select + color + risk tags | **Well-grounded** (`replace_risk_case.rb` production force; `update_severity_force`) |
|
|
49
|
+
| 6 | `select-drives-gauge` | Select (single) | map chosen option → numeric Gauge value + band color | **Well-grounded** (Gauge stops in TemplateBuilder; simpler slice of #5) |
|
|
50
|
+
| 7 | `computed-number` | Number(s) / Select | compute a Number/Gauge from arithmetic over inputs | **Well-grounded** (`overall-risk-mitigation` = arithmetic diff of ratings in `replace_risk`) |
|
|
51
|
+
| 8 | `person-plus-role` | People | reveal a role/detail field per attached person or role Select | **Plausible** (People field + config real; pairing force inferred) |
|
|
52
|
+
| 9 | `cross-reference-with-detail` | CrossReference | on attach, reveal detail/linked fields; optionally pull linked values | **Well-grounded** (CrossReference 2nd most-used; `risk_mapping` attaches hazards/controls; `addLinkedFieldConfig`) |
|
|
53
|
+
| 10 | `select-drives-tags` | Select / Checklist | chosen answer(s) add/remove page tags (register membership) | **Well-grounded** (`update_severity_force` NOTIFIABLE; `tag-on-level` in `replace_risk`; memory: register membership via tags) |
|
|
54
|
+
| 11 | `signoff-block` | Signature (+ People + Date) | reveal/require signature block when a gate answer is set | **Plausible** (Signature field real; conditional-required is force-level, see constraint) |
|
|
55
|
+
|
|
56
|
+
Grounding legend: **Well-grounded** = the field shapes AND the force/flow behaviour were seen in
|
|
57
|
+
production ooze scripts or are directly emitted by `TemplateBuilder`. **Plausible** = field types
|
|
58
|
+
are real and the pattern is idiomatic, but the exact force wiring was inferred, not observed.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 1. `conditional-detail-select`
|
|
63
|
+
|
|
64
|
+
| Aspect | Spec |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **Intent / when to use** | The canonical "if you picked X, tell us more". A single-select gate whose non-default answer(s) reveal and (force-)require a detail field. The workhorse archetype. |
|
|
67
|
+
| **Parent question type** | `Select` (single; `config.select.multiple = false`) |
|
|
68
|
+
| **Dependent fields** | 1+ detail fields — typically `PlainText` (free text) or `RichText` (formatted), hidden by default. Role: capture the "more info". |
|
|
69
|
+
| **Flow / force behaviour** | On chosen answer(s): `show(detail)` + (force-enforced) required. On other answers: `hide(detail)` + `clear(detail)`. Mirrors `replace_risk`'s `(do fld (hide) (clear))` idiom. |
|
|
70
|
+
| **`when` (JSON-Logic)** | `{"in": [{"var": "parent.selected"}, ["Yes"]]}` → effects `[{show_field: detail}, {set_required: detail}]`; else `[{hide_field: detail}, {set_value: {field: detail, value: null}}]` |
|
|
71
|
+
| **Parameters** | `parent_label`, `options[]`, `reveal_on[]` (subset of options), `detail_fields[] {label, type, required_when_shown}` |
|
|
72
|
+
| **Compiles to** | `addField(Select)` + `addSelectFieldOption`×N + `editFieldConfiguration(select.multiple=false)`; `addField(PlainText/RichText)` per detail (hidden); one `addForce` + `addBinding` for parent and each detail. |
|
|
73
|
+
| **Notes / open Qs** | Default hidden state: is "hidden until shown" a template attribute or purely force-driven? `editFieldConfiguration` has `hideView` (base key) — likely the static default; force flips it. Needs confirmation. |
|
|
74
|
+
|
|
75
|
+
## 2. `yes-no-with-evidence`
|
|
76
|
+
|
|
77
|
+
| Aspect | Spec |
|
|
78
|
+
|---|---|
|
|
79
|
+
| **Intent / when to use** | "Did X happen? If yes, attach proof." Compliance/audit checks that need a photo, file, or short note only on the affirmative branch. |
|
|
80
|
+
| **Parent question type** | `Select` (Yes/No) or a single `Checklist` item |
|
|
81
|
+
| **Dependent fields** | `File` and/or `ImageGallery` (evidence) + optional `PlainText` (note). Hidden by default. |
|
|
82
|
+
| **Flow / force behaviour** | `answer == "Yes"` → `show(evidence)` + force-required (at least one attachment); `"No"` → `hide` + `clear`. |
|
|
83
|
+
| **`when` (JSON-Logic)** | `{"==": [{"var": "parent.selected"}, "Yes"]}` → `[{show_field: evidence}, {set_required: evidence}]` |
|
|
84
|
+
| **Parameters** | `parent_label`, `evidence_kind` (`file`\|`image`\|`both`), `note?` (bool), `required_when_shown` |
|
|
85
|
+
| **Compiles to** | Select + 2 options; `addField(File)` / `addField(ImageGallery)` + optional `PlainText`; force with bindings. |
|
|
86
|
+
| **Notes / open Qs** | Specialisation of #1 with fixed Yes/No parent and attachment detail. File/Image field read/write key asymmetry is a known gem bug (memory `model_input_converter_mapping`) but does not affect **template-build** authoring. |
|
|
87
|
+
|
|
88
|
+
## 3. `checklist-with-followup`
|
|
89
|
+
|
|
90
|
+
| Aspect | Spec |
|
|
91
|
+
|---|---|
|
|
92
|
+
| **Intent / when to use** | An inspection checklist where any item marked non-compliant ("No"/unchecked) must open a corrective-detail field for that item. |
|
|
93
|
+
| **Parent question type** | `Checklist` (multi-item, each item = label + checked) |
|
|
94
|
+
| **Dependent fields** | One follow-up field per item (usually `PlainText`), hidden until its item triggers. |
|
|
95
|
+
| **Flow / force behaviour** | For each item: `item.checked == false` (or a "fail" state) → `show(followup_i)` + force-required; else `hide` + `clear`. Iterated in the force the way `replace_risk`'s `map` over unit lists does. |
|
|
96
|
+
| **`when` (JSON-Logic)** | per item: `{"==": [{"var": "checklist.items.i.checked"}, false]}` → `[{show_field: followup_i}, {set_required: followup_i}]` |
|
|
97
|
+
| **Parameters** | `checklist_label`, `items[] {label}`, `followup_type`, `fail_state` (`unchecked`\|`checked`) |
|
|
98
|
+
| **Compiles to** | `addField(Checklist)` (items are the checklist's own config); N `addField(PlainText)`; one force binding all items + all follow-ups. |
|
|
99
|
+
| **Notes / open Qs** | Checklist item ids are instance-assigned; force must bind by ref/label. Whether "N follow-ups + 1 force" or "N small forces" is cleaner is an open authoring choice. |
|
|
100
|
+
|
|
101
|
+
## 4. `multi-select-fanout`
|
|
102
|
+
|
|
103
|
+
| Aspect | Spec |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **Intent / when to use** | A multi-select "which of these apply?" where **each** selected option turns on a dedicated section/field-group; unselected ones are disabled/cleared. |
|
|
106
|
+
| **Parent question type** | `Select` (multiple; `config.select.multiple = true`) or `Checklist` |
|
|
107
|
+
| **Dependent fields** | One **section** (or field cluster) per option — e.g. the `replace_risk` per-`category` units (`equ`, `haz`, `cvp`, `env`, `peo`), each with its own like/consequence/gauge/rating fields. |
|
|
108
|
+
| **Flow / force behaviour** | Directly from `replace_risk`'s `update-unit`: `(active (=sel category kind))` → if active, evaluate/show the section; if not, `(map-1 disable isec rsec)` + `(map-1 clear ...)`. |
|
|
109
|
+
| **`when` (JSON-Logic)** | per option: `{"in": ["equ", {"var": "parent.selected"}]}` → `[{show_section: sec_equ}]`; else `[{hide_section: sec_equ}, {set_value: {field: sec_equ.*, value: null}}]` |
|
|
110
|
+
| **Parameters** | `parent_label`, `options[]`, `section_template` (fields each option's section contains), `multiple: true` |
|
|
111
|
+
| **Compiles to** | Select(multiple) + options; per option a `addSection`+`addStageSection`+ its `addField`s; one force reading the parent and binding every fanned-out section. |
|
|
112
|
+
| **Notes / open Qs** | This is the compositional heart of the risk template — #5 is usually **nested inside** each fanned-out section. Section-level show/hide is the effect that most needs adding to EPTIR's grammar (DESIGN_NOTE §5a). |
|
|
113
|
+
|
|
114
|
+
## 5. `risk-matrix`
|
|
115
|
+
|
|
116
|
+
| Aspect | Spec |
|
|
117
|
+
|---|---|
|
|
118
|
+
| **Intent / when to use** | Classic likelihood × consequence risk rating. Two selects feed a lookup matrix that computes a rating, maps it to a level + color, and tags the page by severity. The single richest observed archetype. |
|
|
119
|
+
| **Parent question type** | Two `Select` fields — `likelihood` and `consequence` (each single-select, ordered options 0..N). |
|
|
120
|
+
| **Dependent fields** | `Gauge` (rating value + colored bands), a level `Select` (Low/Med/High/Very — auto-selected, locked), optional hidden `Number` rating field. |
|
|
121
|
+
| **Flow / force behaviour** | Straight from `replace_risk_case.rb`: `(risk-rating con lik)` indexes a hard-coded matrix; `(risk-level rating)` buckets it; `set-risk` does `(do gau (lock) (set n))`, `(do lev (lock) (clear) (select level) (set-color level))`; `tag-on-level` adds `INHERENT HIGH`/`RESIDUAL HIGH` tags. |
|
|
122
|
+
| **`when` (JSON-Logic)** | Not a boolean gate — a **computed** effect: `{set_value: {field: gauge, value: {"matrix_lookup": [{"var": "likelihood.weight"}, {"var": "consequence.weight"}, <matrix>]}}}` + threshold→level + threshold→tag rules. |
|
|
123
|
+
| **Parameters** | `likelihood_options[]`, `consequence_options[]`, `matrix[][]` (rating table), `level_bands[] {min, label, color, tag?}`, `gauge_max` |
|
|
124
|
+
| **Compiles to** | 2× Select+options; `addField(Gauge)` + `addGaugeFieldStop` per band (threshold+color); level Select+options; `editFieldConfiguration(gauge.max)`; one force encoding the matrix + level LUT + tag rules, bound to all fields. |
|
|
125
|
+
| **Notes / open Qs** | Real forces support **inherent vs residual** pairs and `min-rating-difference` "uncontrolled risk" logic (`uncontrolled` fn) — a richer parametrization than a single matrix. Draft models one matrix; flag the inherent/residual variant as a parameter to add. Matrix values are the archetype's key parameter. |
|
|
126
|
+
|
|
127
|
+
## 6. `select-drives-gauge`
|
|
128
|
+
|
|
129
|
+
| Aspect | Spec |
|
|
130
|
+
|---|---|
|
|
131
|
+
| **Intent / when to use** | A single categorical answer maps to a numeric score shown on a colored gauge (severity score, maturity level). The 1-dimensional slice of #5. |
|
|
132
|
+
| **Parent question type** | `Select` (single) with weighted options. |
|
|
133
|
+
| **Dependent fields** | `Gauge` (score + bands). |
|
|
134
|
+
| **Flow / force behaviour** | `set(gauge, selected_option.weight)` + gauge stops color it; lock the gauge. Same `(do gau (lock) (set n))` idiom. |
|
|
135
|
+
| **`when` (JSON-Logic)** | `{set_value: {field: gauge, value: {"var": "parent.selected.weight"}}}` |
|
|
136
|
+
| **Parameters** | `parent_label`, `options[] {label, weight}`, `gauge_max`, `bands[] {threshold, color}` |
|
|
137
|
+
| **Compiles to** | Select + weighted options (`addSelectFieldOption {label, weight}`); `addField(Gauge)` + `addGaugeFieldStop`s + `editFieldConfiguration(gauge.max)`; force binding select→gauge. |
|
|
138
|
+
| **Notes / open Qs** | Option `weight` is a real `addSelectFieldOption` key — the natural carrier of the numeric mapping, so the force may just read the weight. Confirm gauge can be force-`set` from weight without an explicit LUT. |
|
|
139
|
+
|
|
140
|
+
## 7. `computed-number`
|
|
141
|
+
|
|
142
|
+
| Aspect | Spec |
|
|
143
|
+
|---|---|
|
|
144
|
+
| **Intent / when to use** | A read-only field whose value is arithmetic over other fields (totals, differences, days-between, cost = qty × rate). |
|
|
145
|
+
| **Parent question type** | None (no gate) — inputs are Number/Date/Select fields; the archetype is the **output** + its formula. |
|
|
146
|
+
| **Dependent fields** | Output `Number` or `Gauge` (locked, hidden-from-edit). |
|
|
147
|
+
| **Flow / force behaviour** | Force computes and `set`s the output, then `(lock)`. Grounded in `replace_risk`'s `overall-risk-mitigation` = `(- max-irating max-rrating)` written via `(do overall-risk-mitigation (lock) (set maxs-difference))`. |
|
|
148
|
+
| **`when` (JSON-Logic)** | `{set_value: {field: out, value: {"-": [{"var": "a"}, {"var": "b"}]}}}` (or `*`, `+`, date-diff op) |
|
|
149
|
+
| **Parameters** | `inputs[]` (field refs), `formula` (arithmetic expr), `output_type` (`number`\|`gauge`) |
|
|
150
|
+
| **Compiles to** | `addField(Number/Gauge)` (output); force reads inputs, computes, sets+locks output. |
|
|
151
|
+
| **Notes / open Qs** | Formula grammar needs bounding (arithmetic + date-diff only?). Overlaps #5 — a risk matrix is a specialised computed value; keep separate because #7 is gate-free and formula-driven. |
|
|
152
|
+
|
|
153
|
+
## 8. `person-plus-role`
|
|
154
|
+
|
|
155
|
+
| Aspect | Spec |
|
|
156
|
+
|---|---|
|
|
157
|
+
| **Intent / when to use** | "Who was involved, and in what capacity" — a People field paired with a role/relationship qualifier per person (witness/injured/first-aider). |
|
|
158
|
+
| **Parent question type** | `People` (single or multi) |
|
|
159
|
+
| **Dependent fields** | A role `Select` (or `PlainText`) capturing the capacity; optionally revealed only once a person is attached. |
|
|
160
|
+
| **Flow / force behaviour** | `people.count > 0` → `show(role)`; optional force-required. |
|
|
161
|
+
| **`when` (JSON-Logic)** | `{">": [{"var": "people.count"}, 0]}` → `[{show_field: role}, {set_required: role}]` |
|
|
162
|
+
| **Parameters** | `people_label`, `singular` (bool), `role_options[]` (or free text), `person_schema_id?` |
|
|
163
|
+
| **Compiles to** | `addField(People)` + `editFieldConfiguration(people.singular/personSchemaId)`; role Select/PlainText; force binding people→role. |
|
|
164
|
+
| **Notes / open Qs** | **Plausible, not observed** — People fields are heavily used (40 hits) but a per-person role force wasn't seen. Per-person (vs per-field) role capture may need a repeating/table structure rather than a single Select; flag for design. |
|
|
165
|
+
|
|
166
|
+
## 9. `cross-reference-with-detail`
|
|
167
|
+
|
|
168
|
+
| Aspect | Spec |
|
|
169
|
+
|---|---|
|
|
170
|
+
| **Intent / when to use** | Attach related records from another register (hazards, controls, assets, permits) and reveal/pull detail once attached. The register-linking backbone. |
|
|
171
|
+
| **Parent question type** | `CrossReference` (to a target register) |
|
|
172
|
+
| **Dependent fields** | Detail fields shown on attach; and/or **linked fields** that mirror values from the referenced page (`addLinkedFieldConfig`). |
|
|
173
|
+
| **Flow / force behaviour** | On attach → `show(detail)`; linked fields auto-populate from the source via linked-field config (not a force). `risk_mapping_case.rb` attaches "Associated hazard" + "Attach controls" cross-refs by id. |
|
|
174
|
+
| **`when` (JSON-Logic)** | `{">": [{"var": "xref.count"}, 0]}` → `[{show_field: detail}]`; linked-field pull is config, not `when`. |
|
|
175
|
+
| **Parameters** | `xref_label`, `register_id`, `single_select_mode`, `display_fields[]`, `linked_fields[] {source_field_key}`, `detail_fields[]` |
|
|
176
|
+
| **Compiles to** | `addField(CrossReference)` + `editFieldConfiguration(crossReference {registerId, singleSelectMode, displayFields, hideCreate, hideAttach})`; `addLinkedFieldConfig {sourceFieldKey, crossReferenceId}` per linked field; optional detail fields + force. |
|
|
177
|
+
| **Notes / open Qs** | Two distinct mechanisms here: **linked fields** (declarative config, pull values) vs **forces** (reveal detail). CrossReference is the 2nd most-used type (66 hits) so this archetype is high-value. Register membership interplay via tags overlaps #10. |
|
|
178
|
+
|
|
179
|
+
## 10. `select-drives-tags`
|
|
180
|
+
|
|
181
|
+
| Aspect | Spec |
|
|
182
|
+
|---|---|
|
|
183
|
+
| **Intent / when to use** | Answering a question changes the page's **tags**, which drives register membership, visibility, and policy routing (notifiable events, high-risk escalation, moving a record between registers). |
|
|
184
|
+
| **Parent question type** | `Select` (single/multi) or `Checklist` |
|
|
185
|
+
| **Dependent fields** | None structural — the effect is on page tags. May pair with a hidden marker. |
|
|
186
|
+
| **Flow / force behaviour** | Chosen answer → `add-tag`/`remove-tag`. Directly from `update_severity_force` (severity → `NOTIFIABLE`) and `replace_risk`'s `tag-on-level` (`(tag-off ...) (tag-on tag)`). |
|
|
187
|
+
| **`when` (JSON-Logic)** | `{"in": [{"var": "severity.selected"}, ["major", "critical"]]}` → `[{add_tag: "NOTIFIABLE"}]`; else `[{remove_tag: "NOTIFIABLE"}]` |
|
|
188
|
+
| **Parameters** | `parent_label`, `option_to_tags` (map answer → tag set), `mutually_exclusive` (bool — clear siblings first) |
|
|
189
|
+
| **Compiles to** | Select/Checklist + options; force reads answer, adds/removes tags. No new fields. |
|
|
190
|
+
| **Notes / open Qs** | `add_tag`/`remove_tag` ARE in EPTIR's existing closed effect set (DESIGN_NOTE §5a) — this archetype compiles cleanly **today**, unlike the show/hide ones. Tag→register-membership is a platform rule (memory `register_membership_via_tags`), so this archetype has real downstream reach. |
|
|
191
|
+
|
|
192
|
+
## 11. `signoff-block`
|
|
193
|
+
|
|
194
|
+
| Aspect | Spec |
|
|
195
|
+
|---|---|
|
|
196
|
+
| **Intent / when to use** | A review/approval gate: capture signature + approver + date, revealed/required once the record reaches an approvable state. |
|
|
197
|
+
| **Parent question type** | `Signature` (with companion `People` approver + `Date`) — often gated by a prior Select ("Ready for sign-off?"). |
|
|
198
|
+
| **Dependent fields** | `Signature`, `People` (approver), `Date` (signed-on). |
|
|
199
|
+
| **Flow / force behaviour** | Gate answer set → `show`/force-require the block; before that, hidden. Note the platform also has native stage sign-off (`editRequiredSignOffs`) — the archetype is the **in-form** variant. |
|
|
200
|
+
| **`when` (JSON-Logic)** | `{"==": [{"var": "gate.selected"}, "Ready"]}` → `[{show_field: signature}, {set_required: signature}, {show_field: approver}, {show_field: date}]` |
|
|
201
|
+
| **Parameters** | `gate_label?`, `require_approver` (bool), `require_date` (bool) |
|
|
202
|
+
| **Compiles to** | Optional gate Select; `addField(Signature)` + `addField(People)` + `addField(Date)`; force binding gate→block. |
|
|
203
|
+
| **Notes / open Qs** | **Plausible, not observed** in ooze scripts. Overlaps with native stage sign-off (`editRequiredSignOffs`, `editRequiredSignOffs` command) and workflow task sign-off — decide whether "sign-off" belongs at the **field/force** layer or the **stage/task** layer. Likely the latter for true approvals; keep this archetype for lightweight in-form acknowledgement. |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Cross-cutting notes
|
|
208
|
+
|
|
209
|
+
- **`required` is force-only.** Repeated because it shapes every reveal archetype: the template
|
|
210
|
+
cannot carry a `required` flag through the command bus, so "required when shown" is always the
|
|
211
|
+
force refusing submission / flagging the field. If the future Workflow Builder conditional-field
|
|
212
|
+
feature (in development, `10_forces_workflow_builder.md`) lands a native conditional-required,
|
|
213
|
+
these archetypes should retarget it and drop the hand-authored force.
|
|
214
|
+
- **Effect grammar gap.** Of the effects these archetypes need — `show_field`, `hide_field`,
|
|
215
|
+
`show_section`, `hide_section`, `set_required`, `set_value`, `add_tag`, `remove_tag` — only
|
|
216
|
+
`set_value`/`add_tag`/`remove_tag` are in EPTIR's current closed set. #1–#9 and #11 depend on
|
|
217
|
+
adding the show/hide/require effects (DESIGN_NOTE §5a). #10 compiles today.
|
|
218
|
+
- **Reverse-classification is blocked.** Clustering the ~300 existing templates back into these
|
|
219
|
+
archetypes needs reading forces off a page, which is the one blocked path
|
|
220
|
+
(`Query::PageWithForces`, DESIGN_NOTE §6). This catalog is therefore an **authoring-first**
|
|
221
|
+
taxonomy; treat archetype detection from existing forces as future work.
|
|
222
|
+
- **Section vs field ownership.** #4 (fanout) and #11 (block) reveal whole **sections**;
|
|
223
|
+
#1/#2/#3/#8 reveal individual **fields**. The macro needs to know which it owns so it can emit
|
|
224
|
+
`addSection`/`addStageSection` vs plain `addField` and bind the force at the right granularity.
|
|
225
|
+
|
|
226
|
+
## Patterns seen repeatedly in real ooze scripts
|
|
227
|
+
|
|
228
|
+
- **Weighted single-selects feeding computed risk** is the dominant real pattern (`replace_risk`,
|
|
229
|
+
`update_severity_force`): likelihood/consequence selects → matrix → gauge + level + color + tags.
|
|
230
|
+
- **`(do <field> (lock) (clear) (select ...) / (set ...) (deindex-empty) (hide))`** is the universal
|
|
231
|
+
force idiom for computed/reveal fields — lock, clear, write, (optionally) hide, and drop empties
|
|
232
|
+
from the index. Every archetype's expansion should assume outputs are locked.
|
|
233
|
+
- **Tags as the escalation/routing mechanism** (`NOTIFIABLE`, `INHERENT HIGH`) appears in both
|
|
234
|
+
incident and risk templates — #10 is genuinely load-bearing, not decorative.
|
|
235
|
+
- **Cross-reference attach + per-answer section enable/disable** (`risk_mapping`, `replace_risk`
|
|
236
|
+
per-`category` units) confirms #4 and #9 as real composite shapes, not just single fields.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Spike — Reverse-Classifying Existing Template Questions into Archetypes
|
|
2
|
+
|
|
3
|
+
> **Question.** Can we read the ~300 existing templates' questions and cluster them back into
|
|
4
|
+
> "categories/archetypes" — where an archetype = *parent question (Select/Checklist) + dependent
|
|
5
|
+
> fields + the force(s) that reveal/require/compute them per answer*? What does that depend on?
|
|
6
|
+
>
|
|
7
|
+
> **Scope.** Feasibility + prerequisites only. No code changes. Companion to
|
|
8
|
+
> `DESIGN_NOTE.md` (which frames the *authoring* direction); this spike is the *reverse* direction.
|
|
9
|
+
> All code claims are cited `file:line` and verified against the current tree
|
|
10
|
+
> (branch `feature/model-input-fixes`, 2026-07-07).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Feasibility verdict
|
|
15
|
+
|
|
16
|
+
**Partially feasible today; fully feasible with one small, already-triaged query-shape fix.**
|
|
17
|
+
|
|
18
|
+
- The **structural half** of an archetype (parent Select/Checklist + its options + adjacent
|
|
19
|
+
dependent fields, plus `genomeSignature` and `ref` as equivalence signals) is **readable now** —
|
|
20
|
+
the page/template read model and its fragments already fetch everything needed.
|
|
21
|
+
- The **flow half** (which answer reveals/requires/computes which field) lives in **Forces**, and
|
|
22
|
+
**reading forces off a page is the one blocked path** — but the blocker is a *client-side
|
|
23
|
+
query-shape bug*, not a missing backend or a missing model. The Force model, bindings model,
|
|
24
|
+
read fragment, and read query all already exist in the gem; the query and fragment simply select
|
|
25
|
+
fields the schema rejects. The fix is enumerated to the line in `.ai-assistance/projects/TODO.md`.
|
|
26
|
+
- Even once forces are readable, the **force `script` is an AngularJS/LISP string** with no
|
|
27
|
+
structured answer→effect representation available today. Extracting `answer == "Yes" → show(F)`
|
|
28
|
+
requires parsing that string. This is the **honest residual risk** and it does not go away with
|
|
29
|
+
the query fix (see §5, §6).
|
|
30
|
+
|
|
31
|
+
So: **structure-only classification is unblocked and gives a useful first pass; authoritative
|
|
32
|
+
classification (with the flow contract) needs (a) the forces-read fix — cheap — and (b) a force
|
|
33
|
+
script parser — the real work.**
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Per-signal reliability assessment
|
|
38
|
+
|
|
39
|
+
Ranked by authoritativeness for identifying an archetype.
|
|
40
|
+
|
|
41
|
+
### 2.1 Force bindings + script — AUTHORITATIVE, but gated + needs parsing
|
|
42
|
+
|
|
43
|
+
A `Force` connects fields/sections via **bindings** and encodes the show/hide/require/compute logic
|
|
44
|
+
in its `script`. This is the *only* signal that actually tells you the flow contract.
|
|
45
|
+
|
|
46
|
+
- **Model exists.** `Base::Force` exposes `id`, `name`, `script`, `customScript`, `weight`, `url`,
|
|
47
|
+
`globalBindingIds`, and `bindings` — `lib/ecoportal/api/graphql/base/force.rb:19-28`.
|
|
48
|
+
- **Bindings expose the field/section link.** `Base::Force::Binding` carries `name` + `referenceId`
|
|
49
|
+
(the bound field/section id) — `lib/ecoportal/api/graphql/base/force/binding.rb:7-14`. The
|
|
50
|
+
collection can look up by name — `.../binding_collection.rb:19-22`.
|
|
51
|
+
- **The page model surfaces forces** — `Interface::BasePage#forces` wraps `doc['forces']` in a
|
|
52
|
+
`Force::Collection`, and the page carries a `hasForces` boolean —
|
|
53
|
+
`lib/ecoportal/api/graphql/interface/base_page.rb:75-83`.
|
|
54
|
+
- **A read fragment + read query exist.** `Fragment::ForceFields` selects
|
|
55
|
+
`id name script customScript globalBindingIds bindings{...}` —
|
|
56
|
+
`lib/ecoportal/api/graphql/fragment/force.rb:6-26`. `Query::PageWithForces` fetches
|
|
57
|
+
`page { ...PageFields; forces { ...ForceFields } }` —
|
|
58
|
+
`lib/ecoportal/api/graphql/query/page_with_forces.rb:31-38`.
|
|
59
|
+
|
|
60
|
+
**Reliability caveats even once readable:**
|
|
61
|
+
- `bindings` give you *which* fields a force touches (the cluster membership) — high value, structured.
|
|
62
|
+
- The `script` gives you the *per-answer effect* — but only as an unparsed AngularJS/LISP string
|
|
63
|
+
(`script` / `customScript` are opaque text). No structured answer→effect map is exposed. See §5.
|
|
64
|
+
- A force applies to a stage only if *all* its bindings resolve in that stage
|
|
65
|
+
(`10_forces_workflow_builder.md:16-21`), so binding→field resolution must be done per stage.
|
|
66
|
+
|
|
67
|
+
### 2.2 Field structure / adjacency — AVAILABLE NOW, heuristic only
|
|
68
|
+
|
|
69
|
+
"A Select followed by N fields in the same section" is readable directly from the page/template read
|
|
70
|
+
model: stages → sections → fields.
|
|
71
|
+
|
|
72
|
+
- Sections and their fields are fetched by the page fragment —
|
|
73
|
+
`lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb:105-131` (`sectionUnion` →
|
|
74
|
+
`ContentSection.dataFields` / `SplitSection.left/rightDataFields`).
|
|
75
|
+
- The gem exposes stage→section→field traversal (`page.stages`, `section.components`,
|
|
76
|
+
`get_by_type`, `get_by_name`) — documented in `07_data_fields.md:152-168`.
|
|
77
|
+
- 20+ concrete field types are dispatched via `Base::Page::DataField::TYPE_MAP`
|
|
78
|
+
(`07_data_fields.md:19-45`); `Select`/`Checklist` parents are identifiable by `__typename`.
|
|
79
|
+
|
|
80
|
+
**Reliability: heuristic.** Adjacency correlates with, but does not prove, a conditional
|
|
81
|
+
relationship. A detail field can sit adjacent to its parent for layout reasons with no force, or a
|
|
82
|
+
force can reveal a field in a *different* section. Adjacency gives you *candidate* clusters to then
|
|
83
|
+
confirm against forces. On its own it will both over-group (adjacent-but-independent fields) and
|
|
84
|
+
miss (force reveals a distant field). **Usable for a first-pass bucketing; fragile as ground truth.**
|
|
85
|
+
|
|
86
|
+
### 2.3 genomeSignature — STRONG-BUT-FALLIBLE equivalence signal (not a clustering signal)
|
|
87
|
+
|
|
88
|
+
- **Fetched today.** The data-field fragment selects `genomeSignature`
|
|
89
|
+
(`fragment/pages/common_page_union.rb:124`) and the model reads it —
|
|
90
|
+
`lib/ecoportal/api/graphql/base/page/data_field.rb:45` (`passthrough :label, :deindex, :genomeSignature`).
|
|
91
|
+
- **Already consumed as a pairing signal.** The diff pairing engine uses it, weighted highest
|
|
92
|
+
(0.5) but explicitly not ground truth — `lib/ecoportal/api/graphql/diff/pairing/signals.rb:24-35`
|
|
93
|
+
and `WEIGHTS` at `:17-22`. The header documents 6 known failure modes
|
|
94
|
+
(`signals.rb:6-12`).
|
|
95
|
+
|
|
96
|
+
**Role for classification: equivalence, not clustering.** genomeSignature tells you *"this field in
|
|
97
|
+
template A is the same field as that field in template B"* — invaluable for saying *"the same
|
|
98
|
+
archetype instance recurs across N templates"* once you've identified it once. It does **not** tell
|
|
99
|
+
you *which fields form a cluster within one template* (that's bindings/adjacency). Treat it as the
|
|
100
|
+
cross-template dedup / recurrence signal, not the intra-template grouping signal. Fallible: reused/
|
|
101
|
+
re-purposed fields keep a stale genome, and newer field types may lack one (`signals.rb:8-12,31`).
|
|
102
|
+
|
|
103
|
+
### 2.4 Select option sets, labels/descriptions, `ref` — SUPPORTING signals
|
|
104
|
+
|
|
105
|
+
- **Option sets** — the parent's option labels/ids are read as part of the field data
|
|
106
|
+
(`Select` typed field; `07_data_fields.md:59`). Useful to name the archetype (e.g. a Yes/No
|
|
107
|
+
reveal) and to map `reveal_on` answers once the script is parsed.
|
|
108
|
+
- **Labels/descriptions** — `label` is fetched (`common_page_union.rb:123`). The DESIGN_NOTE notes
|
|
109
|
+
the platform's identity trick is a hidden section-marker field + field `description` as keys
|
|
110
|
+
(`DESIGN_NOTE.md:96-98`) — so `description` doubles as an authored archetype/section key when present.
|
|
111
|
+
- **`ref`** — the ES index key, `<type>.<hash_of_label>` (`07_data_fields.md:181-197`). Stable-ish
|
|
112
|
+
per label; **breaks when the label changes** and can be per-instance if a field was script-added
|
|
113
|
+
without copying from template (`07_data_fields.md:194-197`). Weak identity aid, not a cluster key.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 3. Is fixing the forces-read blocker a PREREQUISITE?
|
|
118
|
+
|
|
119
|
+
**For authoritative reverse-classification (archetype WITH its flow contract): YES.**
|
|
120
|
+
The flow half of an archetype is defined by forces (bindings + script). Bindings are the structured
|
|
121
|
+
statement of *which fields move together*; without reading them you are guessing cluster membership
|
|
122
|
+
from adjacency alone. There is no other exposed source of the conditional flow on the platform today
|
|
123
|
+
(the Workflow Builder's conditional-field workflows that would replace forces are still *in
|
|
124
|
+
development* — `10_forces_workflow_builder.md:83-96`, `86-88`).
|
|
125
|
+
|
|
126
|
+
**For a structure-only first pass (adjacency + genome + options): NO.**
|
|
127
|
+
You can bucket questions into *candidate* archetypes from the read model alone (§2.2–2.4) without any
|
|
128
|
+
forces access. This is useful for triage and to size the problem, but it is **fragile** (see §2.2):
|
|
129
|
+
it cannot distinguish "Select with a genuinely conditional follow-up" from "Select that happens to
|
|
130
|
+
sit above an always-visible field," and it will miss cross-section reveals. Any number you report
|
|
131
|
+
from adjacency-only is a *lower-confidence estimate*, not a classification.
|
|
132
|
+
|
|
133
|
+
**Net:** the forces-read fix is a **prerequisite for the real deliverable** but **not** for an
|
|
134
|
+
initial scoping pass. Given the fix is cheap (§4), there is little reason to ship the fragile
|
|
135
|
+
version as anything more than a scoping estimate.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 4. Minimal-unblock sketch (forces read)
|
|
140
|
+
|
|
141
|
+
The blocker is a **client query-shape bug**, fully triaged in `.ai-assistance/projects/TODO.md:14-24`
|
|
142
|
+
(verified there against the live schema on 2026-07-03 — "all THREE are REAL bugs"). Two edits:
|
|
143
|
+
|
|
144
|
+
1. **`Query::PageWithForces` selects `forces` directly on a union.** The query hangs `forces` off
|
|
145
|
+
the `PageUnion`, which is invalid GraphQL (can't select a field on a bare union) —
|
|
146
|
+
`query/page_with_forces.rb:33-35` (`forces { spread :ForceFields }`). **Fix:** wrap in the shared
|
|
147
|
+
interface — `... on BasePageInterface { forces { ...ForceFields } }` (per TODO.md:16-17).
|
|
148
|
+
|
|
149
|
+
2. **`Fragment::ForceFields` selects `id` on binding types that have none.** The fragment selects
|
|
150
|
+
`id` inside `... on DataFieldBinding` and `... on SectionBinding`
|
|
151
|
+
(`fragment/force.rb:14-23`), but those types expose only `name`/`referenceId` (no `id`) —
|
|
152
|
+
`TODO.md:16-17`. **Fix:** drop `id` from the binding selections (keep `name`, `referenceId`).
|
|
153
|
+
Note `Base::Force::Binding` declares `passkey :id` (`base/force/binding.rb:8`) — harmless once the
|
|
154
|
+
query stops requesting it, since the model tolerates a missing key.
|
|
155
|
+
|
|
156
|
+
3. **(eco-helpers, sibling repo — not this gem)** Split the capability gate.
|
|
157
|
+
`OozeRedirect.force_support?` hard-returns `false`, which gates **read and write together**, so
|
|
158
|
+
the broken read disables the working write as collateral — `10_forces_workflow_builder.md:50-54`,
|
|
159
|
+
RCA `.../analysis/2026-07-01-forces-via-workflow-commands-miss-rca.md:28-36,59-70`. For
|
|
160
|
+
*reverse-classification* we only need the **read** path, so this repo's job is items 1–2; the
|
|
161
|
+
eco-helpers gate split matters for the ooze scripts, not for a classifier that calls
|
|
162
|
+
`Query::PageWithForces` directly.
|
|
163
|
+
|
|
164
|
+
**Verification step (not code):** these three queries have no render/validation specs; the same TODO
|
|
165
|
+
item asks to add a render + schema-validation spec alongside the fix and to wire
|
|
166
|
+
`tests/validate_queries.rb` into CI (`TODO.md:5-24`). Do that first so the fix is provably
|
|
167
|
+
schema-valid against a fresh introspected schema before relying on it against 300 templates.
|
|
168
|
+
|
|
169
|
+
**Effort:** two one-line selection edits + specs. Small. The model/fragment/query scaffolding is all
|
|
170
|
+
already present (shipped as WIP alongside the write path in v1.3.11 per `CHANGELOG.md:184`).
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 5. Parsing the force script — the real difficulty
|
|
175
|
+
|
|
176
|
+
Reading forces gets you the **bindings** (structured: which fields) and the **script** (unstructured:
|
|
177
|
+
what happens per answer). The archetype's *per-answer effect* — `reveal_on: "Yes" → show + require`
|
|
178
|
+
in the DESIGN_NOTE worked example (`DESIGN_NOTE.md:44-56`) — lives **only in the script string**.
|
|
179
|
+
|
|
180
|
+
- `script`/`customScript` are opaque text (`base/force.rb:20`); the gem treats them as strings it
|
|
181
|
+
round-trips, never parses (write path just sets `customScript` and queues `editForce` —
|
|
182
|
+
`base/force.rb:33-38,55-60`). There is **no structured effect representation** exposed anywhere.
|
|
183
|
+
- The scripts are **AngularJS/LISP snippets** (`10_forces_workflow_builder.md:9`,
|
|
184
|
+
`base/force.rb:6`). Extracting a reliable `answer → {show|hide|require|compute}(field)` mapping
|
|
185
|
+
from arbitrary such snippets is an **interpreter/parser problem**, with a long tail of hand-written
|
|
186
|
+
variants across 300 templates and 10+ orgs (~22 force-dependent ooze cases catalogued —
|
|
187
|
+
RCA `:27`). Expect: bespoke idioms, computed risk-matrix math, multi-condition guards, indirection
|
|
188
|
+
through global bindings (`globalBindingIds` — `base/force.rb:22`).
|
|
189
|
+
- The **bindings alone** are a meaningful partial win: they tell you the *cluster membership* (this
|
|
190
|
+
force ties this Select to these N fields/sections) with high confidence, even if you never parse
|
|
191
|
+
the script. That yields "these fields form a conditional cluster governed by force X" — enough to
|
|
192
|
+
identify *that* an archetype exists and its members, if not its exact per-answer semantics.
|
|
193
|
+
|
|
194
|
+
**Structured alternative:** the **Workflow Builder** conditional-field workflows are the intended
|
|
195
|
+
structured replacement for force scripts — but they are **in development, not deployed**
|
|
196
|
+
(`10_forces_workflow_builder.md:83-96`) and the API is explicitly "not stable enough for scripting
|
|
197
|
+
against its internals" (`:98-104`). So there is **no structured flow representation available today**
|
|
198
|
+
for existing templates; existing conditional logic is in AngularJS forces. Reverse-classification of
|
|
199
|
+
the *installed base* cannot wait for Workflow Builder without leaving the ~300 current templates
|
|
200
|
+
uncovered.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 6. Open risks (honest)
|
|
205
|
+
|
|
206
|
+
1. **AngularJS script parsing (highest).** No structured effect map exists; extracting
|
|
207
|
+
answer→effect from arbitrary LISP/AngularJS snippets across 300 templates is the dominant unknown.
|
|
208
|
+
Mitigate by (a) classifying on **bindings + adjacency + options** first (cluster membership +
|
|
209
|
+
candidate parent), and (b) parsing scripts only for a **closed catalog** of common idioms,
|
|
210
|
+
flagging the long tail as "custom force — unclassified" rather than mis-parsing. This mirrors the
|
|
211
|
+
DESIGN_NOTE's closed-catalog recommendation (`DESIGN_NOTE.md:126-130`).
|
|
212
|
+
2. **Forces-read fix unproven against a live org.** The fix is triaged from schema introspection but
|
|
213
|
+
the query has never run green against a force-bearing page (it shipped as WIP —
|
|
214
|
+
`TODO.md:24`, RISKS `.../ooze-graphql-native-migration/RISKS.md:19-30`). Live-test
|
|
215
|
+
`Query::PageWithForces` on a real force-bearing page before trusting bulk output.
|
|
216
|
+
3. **Binding→field resolution is per-stage and can dangle.** A force shows only if all bindings
|
|
217
|
+
resolve in the stage (`10_forces_workflow_builder.md:16-21`); bindings referencing removed/renamed
|
|
218
|
+
fields will dangle. Classifier must tolerate unresolved `referenceId`s.
|
|
219
|
+
4. **genomeSignature fallibility for recurrence.** Re-purposed fields keep a stale genome and newer
|
|
220
|
+
types may lack one (`signals.rb:8-12,31`) — so "same archetype across templates" via genome will
|
|
221
|
+
have false matches/misses. Confirm with type+label+options (the existing pairing engine already
|
|
222
|
+
does exactly this weighting — `signals.rb:17-22`).
|
|
223
|
+
5. **`ref` instability.** Label-hash based and per-instance when script-added
|
|
224
|
+
(`07_data_fields.md:194-197`) — do **not** use `ref` as a cluster or identity key; it is at best a
|
|
225
|
+
weak tiebreaker.
|
|
226
|
+
6. **Forces are being deprecated mid-flight.** New templates use Workflow Builder, old ones use
|
|
227
|
+
forces (`10_forces_workflow_builder.md:61-65`). A classifier built purely on force-reading will
|
|
228
|
+
have blind spots on newer Workflow-Builder templates once conditional workflows deploy — plan for
|
|
229
|
+
a second reader against `PagesWorkflow` when that API stabilises.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 7. Bottom line
|
|
234
|
+
|
|
235
|
+
- **Structure-only classification:** doable now, no gem change — but a fragile first-pass estimate.
|
|
236
|
+
- **Authoritative classification (with flow):** requires the **forces-read fix** (two one-line
|
|
237
|
+
selection edits in `Query::PageWithForces` / `Fragment::ForceFields`, already triaged in
|
|
238
|
+
`TODO.md:14-24`, plus a render/validation spec) — **cheap and low-risk**.
|
|
239
|
+
- **The genuine hard part** is not the read fix; it is **parsing AngularJS force scripts** into
|
|
240
|
+
answer→effect mappings. Bindings give cluster membership cheaply; the per-answer semantics require
|
|
241
|
+
a parser, best scoped to a **closed catalog** of common idioms with a raw-force escape hatch.
|
|
242
|
+
- **genomeSignature** is the cross-template recurrence signal (already wired in the diff pairing
|
|
243
|
+
engine), not the intra-template grouping signal.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Design Note — A Template Model That Holds the Logic
|
|
2
|
+
|
|
3
|
+
> For the team meeting. Goal: *"identify a template model that holds the logic — not just
|
|
4
|
+
> questions and options, but the fields that get unhidden depending on the answer."*
|
|
5
|
+
> Audience: engineers. Keep it skimmable.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. The problem
|
|
10
|
+
|
|
11
|
+
Our template representations today capture **structure** — stages, sections, fields, and (for
|
|
12
|
+
Selects/Checklists) their options. What they do **not** capture is the **conditional flow**: the
|
|
13
|
+
fact that answering a parent question a certain way reveals follow-up fields, makes them required,
|
|
14
|
+
or computes a value. On the platform that flow lives in **Forces** (AngularJS snippets bound to a
|
|
15
|
+
stage: show/hide sections/fields, conditional-required, computed values — being replaced by the
|
|
16
|
+
Workflow Builder's "conditional field workflows", still in development). See
|
|
17
|
+
`.ai-assistance/code/ecoPortal_architecture/10_forces_workflow_builder.md`. A template model that
|
|
18
|
+
omits forces omits the actual behaviour of the form. This meeting is about giving the model a
|
|
19
|
+
first-class place to hold that logic.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. The category / composite question archetype
|
|
24
|
+
|
|
25
|
+
**Reframe Oscar's "category" as a composite question archetype.** A category is not a field type —
|
|
26
|
+
it is a **pattern one level above the flat field**:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
archetype = parent question (usually Select / Checklist)
|
|
30
|
+
+ its options
|
|
31
|
+
+ the dependent fields it governs
|
|
32
|
+
+ the force(s) that reveal / require / compute those fields per answer
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The **category name** identifies the pattern; the category **carries the flow contract** — which
|
|
36
|
+
answer unhides which fields, which answers flip a field to required, which answers drive a computed
|
|
37
|
+
value. It is a **higher-order primitive above the flat field level**: a cluster of fields plus the
|
|
38
|
+
force wiring that moves them together, referenced by name and parameters instead of spelled out.
|
|
39
|
+
|
|
40
|
+
### Worked example — "conditional-detail Select"
|
|
41
|
+
|
|
42
|
+
Author intent (one row / one macro):
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
category: conditional_detail_select
|
|
46
|
+
parent_label: "Was anyone injured?"
|
|
47
|
+
options: ["No", "Yes"]
|
|
48
|
+
reveal_on: "Yes"
|
|
49
|
+
detail_field: { label: "Describe the injury", type: plainText, required_when_shown: true }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Expands to the flat model:
|
|
53
|
+
|
|
54
|
+
- a `Select` field `Was anyone injured?` with options `No`, `Yes`
|
|
55
|
+
- a `PlainText` field `Describe the injury`, hidden by default
|
|
56
|
+
- a **force**: `when answer == "Yes"` → `show(detail_field)` + `set_required(detail_field)`
|
|
57
|
+
|
|
58
|
+
One category, authored once, produces **two fields + one force with two effects**. Change the
|
|
59
|
+
category and all of that regenerates coherently.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Where it sits in the pipeline
|
|
64
|
+
|
|
65
|
+
The category is a **macro that expands into `fields + forces`** — it lives *above* the flat IR, not
|
|
66
|
+
beside it. Layering:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
requirements
|
|
70
|
+
↓
|
|
71
|
+
form design
|
|
72
|
+
↓
|
|
73
|
+
CATEGORIES (composite archetypes: parent + options + dependents + forces) ← the new layer
|
|
74
|
+
↓ expand
|
|
75
|
+
flat IR (EPTIR: fields + tags + forces) ← existing (parked)
|
|
76
|
+
↓ compile
|
|
77
|
+
ecoPortal implementation (WorkflowCommandInput command-bus) ← shipped
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The flat IR already exists in draft as **EPTIR** (ecoPortal Template Intermediate Representation,
|
|
81
|
+
parked in `C:\ruby_scripts\git\eptir`, v0.1 / `2026.1` DRAFT). The category layer does not replace
|
|
82
|
+
EPTIR — it **compiles down into it**. `Builder::TemplateBuilder` + `Diff::CommandSynthesizer`
|
|
83
|
+
already compile a flat declarative spec into `WorkflowCommandInput` batches, so the bottom two
|
|
84
|
+
layers are real; categories add a compile step on top.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 4. What it buys us
|
|
89
|
+
|
|
90
|
+
- **Compresses the CSV pipeline.** The CSV→~300-templates work
|
|
91
|
+
(`.ai-assistance/projects/api-v2-to-graphql-migration/notes/csv-template-pipeline-design.md`,
|
|
92
|
+
delivery Sept 2026) today needs one row per field plus separate spelling-out of every dependent
|
|
93
|
+
field and force. A category row references `category + params` and expands to the whole cluster —
|
|
94
|
+
far fewer rows, far less force hand-authoring, mistakes localised to the macro.
|
|
95
|
+
- **A natural pairing / identity anchor.** A category is a cluster of fields that move together, so
|
|
96
|
+
it is the obvious identity unit. It rides directly on the existing identity tricks — the
|
|
97
|
+
hidden section-marker field (section key) and field `description` (field key) — by anchoring the
|
|
98
|
+
whole cluster to one category key instead of tracking N loose fields + forces independently.
|
|
99
|
+
- **A cleaner diff unit.** A change becomes *"this question's category changed"* or *"a category
|
|
100
|
+
parameter changed"* — one semantic delta — instead of a shower of add/remove field + add/edit
|
|
101
|
+
force/binding deltas. This is exactly the granularity the template diff-deploy work
|
|
102
|
+
(`.ai-assistance/projects/template-diff-deploy/`) wants.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. The three decisions to nail in the meeting
|
|
107
|
+
|
|
108
|
+
**(a) Grow the forces effect grammar.** EPTIR's forces are modelled as a JSON-Logic `when` plus a
|
|
109
|
+
**closed effect set of `add_tag / remove_tag / set_value`**. That set does **not** include
|
|
110
|
+
`show/hide field|section` or `set_required` — which is precisely the flow Oscar is describing. The
|
|
111
|
+
category concept cannot expand into EPTIR until the effect grammar gains `show_field` / `hide_field`
|
|
112
|
+
/ `show_section` / `hide_section` / `set_required` (all still closed, statically analysable). Decide
|
|
113
|
+
this and it flows into the parked EPTIR spec.
|
|
114
|
+
|
|
115
|
+
**(b) Which direction do we solve first — authoring or reverse-classification?** The same taxonomy
|
|
116
|
+
serves two uses:
|
|
117
|
+
- **Authoring** (design → categories → template): forward, and the write path is shipped
|
|
118
|
+
(`WorkflowAddForceInput`/`editForce`/`addBinding` via `executeWorkflowCommands`).
|
|
119
|
+
- **Reverse-classification** (read ~300 existing templates → cluster field+force clusters back into
|
|
120
|
+
categories): depends on **reading forces off a page**, which is the **one blocked path**
|
|
121
|
+
(see §6).
|
|
122
|
+
|
|
123
|
+
Recommend committing to **authoring-first**: it is unblocked, it directly serves the Sept 2026
|
|
124
|
+
CSV delivery, and it lets us validate the taxonomy before we depend on the blocked read path.
|
|
125
|
+
|
|
126
|
+
**(c) Closed catalog vs open user-defined archetypes.** A fixed catalog of ~N archetypes
|
|
127
|
+
(conditional-detail Select, mutually-exclusive Checklist reveal, computed risk matrix, …) vs. an
|
|
128
|
+
open, user-definable system. **Recommend closed-first** given the Sept 2026 deadline: a small closed
|
|
129
|
+
catalog is faster to build, trivial to conformance-test, and covers the common cases; leave an
|
|
130
|
+
escape hatch (raw fields+forces) for the long tail and revisit openness later.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 6. The one concrete blocker
|
|
135
|
+
|
|
136
|
+
**Reading forces off a page is blocked.** `Query::PageWithForces` (used by
|
|
137
|
+
`ForceCompat#with_each_entry`) currently fails **client-side schema validation** (PageUnion
|
|
138
|
+
selections, `id` on binding types) — `10_forces_workflow_builder.md` lines 46-54. **Writing** forces
|
|
139
|
+
works today via the command bus. So authoring-first is fully unblocked; any reverse-classification of
|
|
140
|
+
existing templates is gated on fixing this read query. Name it in the room so ownership is clear.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 7. Recommendation
|
|
145
|
+
|
|
146
|
+
1. Adopt the **composite question archetype ("category")** as a new layer that compiles down into
|
|
147
|
+
the flat fields+forces IR (EPTIR) — do not fold it into the flat layer.
|
|
148
|
+
2. **Extend EPTIR's forces effect grammar** with `show/hide field|section` + `set_required`
|
|
149
|
+
(decision 5a) — this is the concrete spec change that unblocks everything else.
|
|
150
|
+
3. Ship a **closed catalog** of a handful of archetypes (5c), **authoring-first** (5b), targeting the
|
|
151
|
+
CSV pipeline for Sept 2026.
|
|
152
|
+
4. File **fixing `Query::PageWithForces`** as the tracked prerequisite for reverse-classification
|
|
153
|
+
(§6), but do not let it block the authoring track.
|
|
154
|
+
```
|