ecoportal-api-graphql 1.3.12 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/bridge/archive/oscar-a1b2c3d-gitlab-mcp-doc-update.inbox.md +29 -29
  3. data/.ai-assistance/bridge/archive/oscar-c912c25-gemini-design-review.inbox.md +42 -42
  4. data/.ai-assistance/projects/compat-layer-audit/COMPAT_AUDIT.md +244 -0
  5. data/.ai-assistance/projects/template-model-logic/CATEGORY_CATALOG.md +236 -0
  6. data/.ai-assistance/projects/template-model-logic/CLASSIFICATION_SPIKE.md +243 -0
  7. data/.ai-assistance/projects/template-model-logic/DESIGN_NOTE.md +154 -0
  8. data/.ai-assistance/scripts/bridge-inbox-check.sh +75 -0
  9. data/.ai-assistance/skills/project-self-docs/SKILL.md +181 -0
  10. data/.ai-assistance/skills/project-self-docs/scripts/self_docs_scan.py +378 -0
  11. data/.ai-assistance/standards-version.json +9 -9
  12. data/.claude/settings.json +150 -146
  13. data/.gitignore +37 -37
  14. data/.gitlab-ci.yml +45 -0
  15. data/CHANGELOG.md +43 -0
  16. data/CLAUDE.md +1 -0
  17. data/docs/self-docs/ARCHITECTURE.md +3 -3
  18. data/docs/self-docs/CHANGES.jsonl +5 -0
  19. data/docs/self-docs/COMPLIANCE.md +79 -0
  20. data/docs/self-docs/CONVENTIONS.md +3 -3
  21. data/docs/self-docs/INTEGRATIONS.md +17 -15
  22. data/docs/self-docs/OPERATIONS.md +76 -0
  23. data/docs/self-docs/OVERVIEW.md +16 -3
  24. data/docs/self-docs/STATUS.md +9 -5
  25. data/docs/self-docs/self-docs-index.json +20 -8
  26. data/lib/ecoportal/api/common/graphql/http_client.rb +189 -177
  27. data/lib/ecoportal/api/graphql/base/location_classification_type.rb +15 -15
  28. data/lib/ecoportal/api/graphql/base/location_structure.rb +27 -27
  29. data/lib/ecoportal/api/graphql/base/organization.rb +15 -15
  30. data/lib/ecoportal/api/graphql/base.rb +35 -35
  31. data/lib/ecoportal/api/graphql/builder/contractor_entity.rb +41 -41
  32. data/lib/ecoportal/api/graphql/connection/action.rb +11 -11
  33. data/lib/ecoportal/api/graphql/connection/action_category.rb +11 -11
  34. data/lib/ecoportal/api/graphql/connection/contractor_entity.rb +11 -11
  35. data/lib/ecoportal/api/graphql/connection/page.rb +11 -11
  36. data/lib/ecoportal/api/graphql/connection/person_member.rb +11 -11
  37. data/lib/ecoportal/api/graphql/connection/preview_page.rb +11 -11
  38. data/lib/ecoportal/api/graphql/connection.rb +17 -17
  39. data/lib/ecoportal/api/graphql/fragment/location_node.rb +26 -26
  40. data/lib/ecoportal/api/graphql/input/contractor_entity.rb +14 -14
  41. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage.rb +18 -18
  42. data/lib/ecoportal/api/graphql/input/workflow_command/edit_field_configuration.rb +21 -21
  43. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page.rb +28 -28
  44. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page_creator_permissions.rb +18 -18
  45. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/cross_reference.rb +23 -23
  46. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/date.rb +20 -20
  47. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/image_gallery.rb +20 -20
  48. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/plain_text.rb +20 -20
  49. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/rich_text.rb +20 -20
  50. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/select.rb +20 -20
  51. data/lib/ecoportal/api/graphql/input/workflow_command/move_field.rb +18 -18
  52. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_forces.rb +18 -18
  53. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_section.rb +18 -18
  54. data/lib/ecoportal/api/graphql/input/workflow_command.rb +251 -251
  55. data/lib/ecoportal/api/graphql/input.rb +24 -24
  56. data/lib/ecoportal/api/graphql/logic/connection.rb +34 -34
  57. data/lib/ecoportal/api/graphql/model/person_member.rb +15 -15
  58. data/lib/ecoportal/api/graphql/model.rb +30 -30
  59. data/lib/ecoportal/api/graphql/mutation/contractor_entity.rb +14 -14
  60. data/lib/ecoportal/api/graphql/payload/contractor_entity.rb +14 -14
  61. data/lib/ecoportal/api/graphql/query/location_structures.rb +57 -57
  62. data/lib/ecoportal/api/graphql/query.rb +27 -27
  63. data/lib/ecoportal/api/graphql_version.rb +1 -1
  64. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74b442ca224440bee94f3f88522a4233b995d187db1b70ba31a0801e0691d3eb
4
- data.tar.gz: 5d5d05fdf46fac5a32a12c5d4c8e47c21b913332bbe04c40a3ab81beeacbab9a
3
+ metadata.gz: 5cbeb13326afff8c35946fd492b9bd6cd050e28819497b643bb4ea6c4b41caca
4
+ data.tar.gz: ad4660f8fa032e08d0c5f09b199f280d8b42a4e234144ad2fab4041b154fe835
5
5
  SHA512:
6
- metadata.gz: 7d667e7ed09fc6ed6fd200695567eb25a8bb5f4f41b3b98882e04d84acd1775c1a17dc7d424e82f26b7f89d4f826bb46c25fddc0ef5fc526e8731aaccd73193b
7
- data.tar.gz: 324b54e96de9f7ae82e71bf1b2c0427fa6fd4ed99d0153ec836a85cf68d76d5ce8f8462865b29cddd446015caa2b4a5fcaf940c5c2866970d7814a2d0495a73b
6
+ metadata.gz: 7337308796e0a6800e9dd678d8b9317978d4cf13f6514b2ba95e5d6eaac95b9833be786ca573b7e59613660077aaa78c808a62c4bbef0c9e2f1a08e0c752e857
7
+ data.tar.gz: 602edaca4d582eeb876549bba42735fc17fc07d832de713fd3ba84234cc2a876aea876beb51c4fb54eef3faf5a334b889443d922d9c6debe196545389b2715e6
@@ -1,29 +1,29 @@
1
- # TASK: Update gitlab-mcp.md with new PAT scopes and rotation info
2
-
3
- STATUS: PENDING
4
- CREATED: 2026-06-04T00:00:00Z
5
- FROM: cowork
6
- TO: code
7
-
8
- ## File to update
9
-
10
- `.claude/integrations/gitlab-mcp.md`
11
-
12
- ## Changes
13
-
14
- ### Scopes table — replace entirely
15
-
16
- | `read_api` | Read MRs, issues, diffs, repository metadata |
17
- | `read_repository`| Clone and read repository contents |
18
- | `create_runner` | Register GitLab CI runners |
19
- | `ai_features` | Access GitLab AI/Duo endpoints |
20
-
21
- ### PAT creation note — replace
22
-
23
- Enable **rotation** at creation time.
24
- **Token rotation:** Current token expires **April 2027**. When rotating, update
25
- in both `.env` and `claude_desktop_config.json`.
26
-
27
- ### "What it doesn't provide" — remove pipeline management item
28
-
29
- `create_runner` + `ai_features` scopes now partially cover CI and AI capabilities.
1
+ # TASK: Update gitlab-mcp.md with new PAT scopes and rotation info
2
+
3
+ STATUS: PENDING
4
+ CREATED: 2026-06-04T00:00:00Z
5
+ FROM: cowork
6
+ TO: code
7
+
8
+ ## File to update
9
+
10
+ `.claude/integrations/gitlab-mcp.md`
11
+
12
+ ## Changes
13
+
14
+ ### Scopes table — replace entirely
15
+
16
+ | `read_api` | Read MRs, issues, diffs, repository metadata |
17
+ | `read_repository`| Clone and read repository contents |
18
+ | `create_runner` | Register GitLab CI runners |
19
+ | `ai_features` | Access GitLab AI/Duo endpoints |
20
+
21
+ ### PAT creation note — replace
22
+
23
+ Enable **rotation** at creation time.
24
+ **Token rotation:** Current token expires **April 2027**. When rotating, update
25
+ in both `.env` and `claude_desktop_config.json`.
26
+
27
+ ### "What it doesn't provide" — remove pipeline management item
28
+
29
+ `create_runner` + `ai_features` scopes now partially cover CI and AI capabilities.
@@ -1,42 +1,42 @@
1
- # TASK: Gemini design review of CoWork↔Code bridge architecture
2
- STATUS: PENDING
3
- CREATED: 2026-06-03T00:00:00Z
4
- FROM: cowork
5
- TO: code
6
-
7
- ## Context
8
- We have designed a file-based bridge system for bidirectional communication between
9
- Claude in CoWork and Claude Code. The bridge lives in .claude/bridge/ inside a git repo.
10
-
11
- Architecture summary:
12
- - Inbox/outbox pattern with UUID+username prefixed filenames (no merge conflicts)
13
- - Scripts (bash + TypeScript) handle file ops, env detection, UUID gen — offloading work from AI tokens
14
- - CLAUDE.md files in each subfolder give each agent scoped instructions
15
- - capabilities/ folder tracks what each environment can do, with a dated assumptions log
16
- - conventions/ folder defines gitignore rules and instruction file structure
17
- - capabilities-check.ts runs periodically using Haiku to flag stale assumptions
18
-
19
- Key design decisions:
20
- - Each agent checks capabilities before bridging (only bridge what you can't do yourself)
21
- - Archive is committed to git — it's the audit trail
22
- - Draft tasks (.draft.md) are gitignored — local-only until pushed
23
- - Code processes only its own username-prefixed tasks
24
-
25
- ## Request
26
- Use the Gemini connector (or Gemini API if available) to get an independent review of
27
- this architecture. Ask Gemini:
28
-
29
- 1. What are the weaknesses or failure modes of this design?
30
- 2. Is the token-offloading strategy (scripts vs AI prose) well-scoped, or are there gaps?
31
- 3. Are there simpler or more robust alternatives for any part of this?
32
- 4. Anything we haven't considered — especially around git workflows, multi-agent coordination,
33
- or long-term maintenance?
34
-
35
- If Gemini connector is not available, use web search to find prior art on
36
- file-based agent coordination patterns and report what you find.
37
-
38
- ## Expected output
39
- - Gemini's (or research-based) critique: weaknesses, gaps, alternatives
40
- - Your own assessment: do you agree with the critique? What would you change?
41
- - Specific suggested amendments to the design (if any)
42
- - Update capabilities/connectors.md with Gemini connector status after attempting this
1
+ # TASK: Gemini design review of CoWork↔Code bridge architecture
2
+ STATUS: PENDING
3
+ CREATED: 2026-06-03T00:00:00Z
4
+ FROM: cowork
5
+ TO: code
6
+
7
+ ## Context
8
+ We have designed a file-based bridge system for bidirectional communication between
9
+ Claude in CoWork and Claude Code. The bridge lives in .claude/bridge/ inside a git repo.
10
+
11
+ Architecture summary:
12
+ - Inbox/outbox pattern with UUID+username prefixed filenames (no merge conflicts)
13
+ - Scripts (bash + TypeScript) handle file ops, env detection, UUID gen — offloading work from AI tokens
14
+ - CLAUDE.md files in each subfolder give each agent scoped instructions
15
+ - capabilities/ folder tracks what each environment can do, with a dated assumptions log
16
+ - conventions/ folder defines gitignore rules and instruction file structure
17
+ - capabilities-check.ts runs periodically using Haiku to flag stale assumptions
18
+
19
+ Key design decisions:
20
+ - Each agent checks capabilities before bridging (only bridge what you can't do yourself)
21
+ - Archive is committed to git — it's the audit trail
22
+ - Draft tasks (.draft.md) are gitignored — local-only until pushed
23
+ - Code processes only its own username-prefixed tasks
24
+
25
+ ## Request
26
+ Use the Gemini connector (or Gemini API if available) to get an independent review of
27
+ this architecture. Ask Gemini:
28
+
29
+ 1. What are the weaknesses or failure modes of this design?
30
+ 2. Is the token-offloading strategy (scripts vs AI prose) well-scoped, or are there gaps?
31
+ 3. Are there simpler or more robust alternatives for any part of this?
32
+ 4. Anything we haven't considered — especially around git workflows, multi-agent coordination,
33
+ or long-term maintenance?
34
+
35
+ If Gemini connector is not available, use web search to find prior art on
36
+ file-based agent coordination patterns and report what you find.
37
+
38
+ ## Expected output
39
+ - Gemini's (or research-based) critique: weaknesses, gaps, alternatives
40
+ - Your own assessment: do you agree with the critique? What would you change?
41
+ - Specific suggested amendments to the design (if any)
42
+ - Update capabilities/connectors.md with Gemini connector status after attempting this
@@ -0,0 +1,244 @@
1
+ # GraphQL COMPAT Layer Audit — contract fidelity vs ecoportal-api-v2
2
+
3
+ > READ-ONLY audit. No code changed, no git touched. Date: 2026-07-08.
4
+ > Scope: every place the GraphQL gem re-expresses a v2 public API, measured against the
5
+ > v2 contract as source of truth. Consumers surveyed: `multi_org_api/*/config/ooze_cases`,
6
+ > eco-helpers `usecases/graphql` + `OozeRedirect` + `ooze_samples`.
7
+
8
+ ---
9
+
10
+ ## 1. Executive verdict — AD-HOC PATCH, not a faithful mirror
11
+
12
+ Oscar is **right**. The compat layer is a set of reactive, per-symptom patches, not a
13
+ contract-faithful mirror of the v2 API. Concrete evidence:
14
+
15
+ 1. **Same method, three different return-type decisions, no shared contract.** `get_by_name`
16
+ is a COLLECTION in v2 for components, forces, and bindings, but a SINGLE for stages. The
17
+ gem got forces (`base/force/collection.rb:16` → `select`), bindings
18
+ (`base/force/binding_collection.rb:19` → `select`), and stages
19
+ (`compat/stage_collection.rb:34` → `find`, correct) right — but got **components wrong**
20
+ (`base/page/data_field/collection.rb:35` → `find`) and **sections' `get_by_heading` wrong**
21
+ (`base/page/section_collection.rb:31` → `find`, v2 is `select`). Three files, four
22
+ independent return-type choices. A faithful mirror would have one rule per v2 method; this
23
+ has ad-hoc choices that happen to match in some files and not others.
24
+
25
+ 2. **The downstream consumer already contains a workaround for the gem's wrong return type.**
26
+ eco-helpers `OozeRedirect#with_fields` wraps the component lookup:
27
+ `[collection.get_by_name(label)].compact` (`ooze_redirect.rb:187`) — i.e. it treats
28
+ `get_by_name` as returning a SINGLE and re-wraps it into an array to fake the v2 collection
29
+ contract. That is a patch compensating for a patch. When the gem is made faithful (returns a
30
+ collection), this line double-wraps → `[[Field]]`. Whack-a-mole in the flesh.
31
+
32
+ 3. **Two conflicting `#components` definitions on the same object.** `Concerns::PageCompat#components`
33
+ returns `dataFields || []` (`page_compat.rb:22`); `Concerns::DataFieldAccess#components`
34
+ returns `field_collection` (`data_field_access.rb:22`). Both are `include`d in
35
+ `Interface::BasePage` (`base_page.rb:6-7`), DataFieldAccess second so it wins — but the dead
36
+ PageCompat version returning a raw array is a fossil of an earlier, incompatible attempt.
37
+
38
+ 4. **Methods the v2 contract guarantees are simply absent** (not deliberately dropped —
39
+ nobody enumerated the contract): `DataField::Collection` has no `get_by_id`, no
40
+ `get_by_type` returning a `reference?`-style API, no `unattached`/`multi_section`;
41
+ `SectionCollection` has no `get_by_id`, no `ordered`-by-weight, no `between`. Consumers call
42
+ `components.get_by_id` and `sections.get_by_id` today (§3).
43
+
44
+ 5. **`components.add` has different semantics** — v2 `upsert!`s into the live collection so a
45
+ later `get_by_name` finds it; the gem stashes into a private `@additions` array
46
+ (`collection.rb:53-60`) that `each`/`get_by_name` never see.
47
+
48
+ The through-line: each method was added when a specific script broke, choosing whatever return
49
+ shape that one script needed — never derived from the v2 method's documented contract. That is
50
+ the definition of ad-hoc.
51
+
52
+ ---
53
+
54
+ ## 2. Divergence catalog
55
+
56
+ Severity: **S1** = silent wrong result / breaks live consumers; **S2** = raises / missing method;
57
+ **S3** = latent (no current consumer but contract-divergent).
58
+
59
+ | # | Method | v2 contract (file:line) | gem compat (file:line) | Divergence | Sev | Affected consumers |
60
+ |---|--------|-------------------------|------------------------|------------|-----|--------------------|
61
+ | C1 | `components.get_by_name(name, type:)` | `select` → **Array** (`v2/page/components.rb:37`) | `find` → **single DataField** (`base/page/data_field/collection.rb:35`) | Return type: collection vs single | **S1** | act-gov `.first`/`.last`, farmers `.first`, and ~120 `components.get_by_name(...).first` call-sites across multi_org_api (twg, tng, briscoes, alexander, argenta, whittlesea…); eco-helpers `ooze_redirect.rb:184,187` |
62
+ | C2 | `sections.get_by_heading(h, mild:)` | `ordered.select` → **Array** (`v2/page/sections.rb:61`) | `find` → **single Section** (`base/page/section_collection.rb:31`) | Return type + drops `mild:` + no weight-ordering | **S1** | `tng_env_update_case.rb:19` (`.get_by_heading("Details").first`) |
63
+ | C3 | `components.get_by_id(id)` | `find` → single (`v2/page/components.rb:21`) | **absent** on `DataField::Collection` | Missing method → `NoMethodError` | **S2** | `event_changes_case.rb:50` (`components.get_by_id(fld.reference_id)`); eco-helpers `ooze_update_case.rb:10` |
64
+ | C4 | `sections.get_by_id(id)` | `find` → single (`v2/page/sections.rb:46`) | **absent** on `SectionCollection` | Missing method → `NoMethodError` | **S2** | `TnG_Active_sitefix_part2_case.rb:106` |
65
+ | C5 | `components.add(...)` | `upsert!` into live collection; found by later `get_by_name`/`each` (`v2/page/components.rb:53`) | stashes in private `@additions`; NOT in `@fields`; later `get_by_name`/`each` miss it (`collection.rb:53-60`) | Mutation semantics diverge | S2 | any multi-step add-then-read script; `add` kwargs also differ (`id:` required, `doc:` shape) |
66
+ | C6 | `sections.get_by_type(t)` | `ordered.select` → Array (`v2/page/sections.rb:53`) | `select` → Array but **not weight-ordered** (`section_collection.rb:25`) | Ordering: v2 orders by weight, gem uses response order | S3 | ordering-sensitive section scripts (latent) |
67
+ | C7 | `components.get_by_type(t)` | `select` → Array, no ordering (`v2/page/components.rb:28`) | `select` → Array (`collection.rb:27`) | **Match** (return type) | ok | argenta `remove_edit_events_case.rb:21,29` (uses `.select` on result — works only because both are Arrays) |
68
+ | C8 | `forces.get_by_name(name)` | `select` → Array (`v2/page/forces.rb:36`) | `select` → Array (`base/force/collection.rb:16`) | **Match** | ok | all `forces.get_by_name(...).first` sites |
69
+ | C9 | `force.bindings.get_by_name(name, type:)` | `select` → Array (`v2/page/force/bindings.rb:102`) | `select` → Array but **drops `type:` kwarg** (`binding_collection.rb:19`) | Partial: arity narrower | S3 | bindings callers pass name only today; `type:` unused |
70
+ | C10 | `stages.get_by_name(name)` | `find` → single (`v2/page/stages.rb:17`) | `find` → single (`compat/stage_collection.rb:34`) | **Match** | ok | eco-helpers `ooze_base_case.rb:109,144`, `register_export_case.rb:137` |
71
+ | C11 | `stages[id_or_name]` | v2 CollectionModel `[]` by key (id) | id **or** name (`stage_collection.rb:27-30`) | Superset (name-fallback) — benign but non-contract | S3 | eco-helpers `stages[id] || stages.get_by_name` (belt-and-braces) |
72
+ | C12 | `forces.get_by_id`, `.ordered`, `.bindings_by_reference` | present (`v2/page/forces.rb:29,64,20`) | **absent** on gem `Force::Collection` | Missing methods | S3 | not called by surveyed consumers (latent) |
73
+ | C13 | `bindings.get_by_id / get_by_type / get_by_reference / reference? / by_name / by_reference` | present (`v2/page/force/bindings.rb:82,92,68,61,26,42`) | **absent** on gem `BindingCollection` | Missing methods | S3 | not called by surveyed consumers (latent) |
74
+ | C14 | `sections.ordered` (by weight) | `sort_by weight` (`v2/page/sections.rb:86`) | returns `@sections` as-is (`section_collection.rb:41`) | Ordering not applied | S3 | latent |
75
+ | C15 | `page.components` (page-level) | v2 components collection | **two conflicting defs**: PageCompat returns `dataFields`/`[]` array (`page_compat.rb:22`), DataFieldAccess returns `Collection` (`data_field_access.rb:22`); include order (`base_page.rb:6-7`) makes DataFieldAccess win, PageCompat one is dead code | Latent footgun / dead code | S2 | any refactor reordering includes silently swaps behaviour |
76
+ | C16 | `entry.components.get_by_name(...).detect{…}` | Array#detect works on v2 collection | gem returns single → `.detect` = `Enumerable#detect` on a **DataField** (iterates its doc?) | Return-type breakage | **S1** | `risks_migration_case.rb:89` (`.get_by_name("Review frequency:").detect{…}`) |
77
+ | C17 | `force.bindings.get_by_name(x).empty?` | Array#empty? | gem Array → ok (C8/C9 correct) | ok | tng `tng_env_update_case.rb:112` |
78
+
79
+ ### Folded-in prior findings (verified against current code)
80
+
81
+ These are not `get_by_name`-style return-type bugs but are part of the same "compat built ad hoc"
82
+ thread — the read/write CONVERTER contract was never systematically mirrored. Cross-referenced
83
+ against `.ai-assistance/code/model_input_mapping/SYNTHESIS.md` (2026-07-07). **Note:** SYNTHESIS
84
+ marks several as FIXED on `feature/model-input-fixes` (current branch); status below reflects that.
85
+
86
+ | # | Prior finding | Current status (verified) | File:line |
87
+ |---|---------------|---------------------------|-----------|
88
+ | P1 | `passarray`/`embeds_many` don't diff via `as_update` (otherTags, locations, filterTags) | **FIXED** — `DIFF_CLASS = Diffable::LeafDiffService` now on `Interface::BasePage` | `interface/base_page.rb:13` |
89
+ | P2 | `IdDiff` set-diff machinery non-functional (ContractorEntity/Action) | Reported FIXED via `id_diff_fields` macro (SYNTHESIS §3.2); **not re-verified in this pass** — flagged for confirm | — |
90
+ | P3 | File/Image/Contractor readers use write-key → `[]` | Reported FIXED (SYNTHESIS §3.3); Image fragment `images[].id` was the blocker | `base/page/data_field/{file_field,image_gallery,contractor_entities}.rb` |
91
+ | P4 | DataField read-key ≠ write-key (shape asymmetry) | Partly FIXED (CrossReference was template); File/Image/Contractor per P3 | `.ai-assistance/code/refactoring/datafield-readwrite-shape-asymmetry.md` |
92
+ | P5 | `SnakeCamelAccess` is a one-way method_missing shim, no specs | **STILL TRUE** — `concerns/snake_camel_access.rb`; no spec file exists (`Glob spec/**/snake_camel*` → none). One-way (snake→camel only); silently no-ops single-word methods (`parts.length < 2` → returns as-is) | `concerns/snake_camel_access.rb:50-54` |
93
+
94
+ ---
95
+
96
+ ## 3. Comprehensive fix plan (contract-faithful) + spec risk
97
+
98
+ The organising principle: **derive each collection method from the v2 method it mirrors, not from
99
+ the one script that broke.** Recommend a small shared helper module so components/sections/forces/
100
+ bindings collections share one contract per method name.
101
+
102
+ ### Fix C1 — `DataField::Collection#get_by_name` → collection (`select`)
103
+
104
+ Faithful fix: return `pool.select { … }` (mirror `v2/page/components.rb:37`), keeping `type:`.
105
+
106
+ **Risks these EXISTING gem specs (they assume single return):**
107
+ - `spec/.../base/page/data_field_spec.rb:397-399` — `coll.get_by_name('name').id` → must become `.first.id`
108
+ - `data_field_spec.rb:408` — `coll.get_by_name('Name').value = 'Bob'` → `.first.value =`
109
+ - `data_field_spec.rb:419` — same
110
+ - `data_field_spec.rb:463` — `field = coll.get_by_name('Name')` → `.first`
111
+ - `base/page/phased/stage_spec.rb:72` — `stage.components.get_by_name('Description').value` → `.first.value`
112
+ - `compat/stage_collection_spec.rb:92` — `stage.components.get_by_name('Description').value` → `.first.value`
113
+ - `compat/stage_view_spec.rb:57` — `view.components.get_by_name('Summary').value` → `.first.value`
114
+ - `input/page/update_spec.rb:136` — `page_with_fields.components.get_by_name('Note').value = 'New'` → `.first.value =`
115
+ - `integration/toocs_submit_replay_spec.rb:38` — `page.components.get_by_name('Coding completed by:')` → `.first`
116
+
117
+ These 9 spec sites LOCK IN the wrong behaviour and must be updated in lockstep with the fix
118
+ (add `.first`). They also confirm the gem's own test suite was written to the ad-hoc behaviour,
119
+ not the v2 contract.
120
+
121
+ **Also requires a coordinated eco-helpers change:** `ooze_redirect.rb:184-187` — line 187
122
+ `[collection.get_by_name(label)].compact` must become `collection.get_by_name(label)` (already an
123
+ Array), and line 184-185 `field = collection.get_by_name(label); … [field] : []` must select over
124
+ the collection. This is the cross-repo lockstep the audit brief warns about: a "just remove
125
+ `.first`" workaround in org scripts is itself a dirty patch that re-breaks here.
126
+
127
+ ### Fix C2 — `SectionCollection#get_by_heading` → collection (`select`), add `mild:`, order by weight
128
+
129
+ Faithful: `ordered.select { same_string?(s.heading, h, mild: mild) }`. Add a `same_string?`
130
+ helper (case-insensitive; `mild:` = alnum-only compare per v2). Requires §Fix-C14 (`ordered` by
131
+ weight) for exact parity.
132
+
133
+ **Spec risk:** no existing gem spec asserts `get_by_heading` (Grep found none) → **no green
134
+ specs go red.** Add new specs (§4). Consumer `tng_env_update_case.rb:19` already does `.first`, so
135
+ it starts working correctly.
136
+
137
+ ### Fix C3 / C4 — add `get_by_id` to `DataField::Collection` and `SectionCollection`
138
+
139
+ `find { |x| x.id == id }` (mirror v2). **No spec risk** (new methods). Add coverage.
140
+
141
+ ### Fix C5 — `components.add` upserts into the live collection
142
+
143
+ Faithful: push the built field into `@fields` (or an upsert-by-id) so later `get_by_name`/`each`
144
+ find it, and keep `dirty_additions` sourced from those. **Spec risk:** check `data_field_spec.rb`
145
+ add-section (`:451-465`) — the deletion test reads `get_by_name('Name')` (existing field, fine);
146
+ the add tests assert `dirty_additions` only, so upserting into `@fields` needs a guard that
147
+ `dirty_inputs` (updates) does not double-count additions. Design the split carefully; lock with a
148
+ "add then get_by_name finds it" spec.
149
+
150
+ ### Fix C6 / C14 — order `get_by_type`/`ordered` by weight in `SectionCollection`
151
+
152
+ Only if sections carry a weight in the GraphQL doc (verify fragment exposes it; if not, response
153
+ order is the only signal and this stays a documented gap). **No current spec risk.**
154
+
155
+ ### Fix C9 / C12 / C13 — fill missing force/binding contract methods + `type:` on bindings
156
+
157
+ Add `get_by_id`, `get_by_type`, `get_by_reference`, `reference?`, `by_name`, `by_reference`,
158
+ `ordered`, `bindings_by_reference`, and honour `type:` in `binding.get_by_name`. These are S3
159
+ (no live consumer) — do them to make the mirror complete, not urgently. **No spec risk.**
160
+
161
+ ### Fix C15 — delete the dead `PageCompat#components`
162
+
163
+ Remove `PageCompat#components` (`page_compat.rb:21-24`); it is shadowed and returns the wrong
164
+ shape if include order ever changes. `page_compat_spec.rb:39-43` asserts `components` is a
165
+ `DataField::Collection` — that stays green (DataFieldAccess provides it). **No spec risk.**
166
+
167
+ ### Fix P5 — add specs (and ideally make two-way) for `SnakeCamelAccess`
168
+
169
+ At minimum add characterization specs. Consider reusing eco-helpers' polished hash-level
170
+ snake/camel module rather than the bespoke `method_missing` shim (per memory
171
+ `project_snake_camel_access`). Scope decision for Oscar.
172
+
173
+ ### Recommended structural change (kills the class of bug)
174
+
175
+ Extract a shared `Concerns::V2CollectionContract` (or a `Base::Page::CollectionModel`) that
176
+ defines `get_by_id` (single), `get_by_name`/`get_by_heading`/`get_by_type` (collection, per v2),
177
+ `ordered`, `same_string?(mild:)` **once**, and have DataField/Section/Force/Binding collections
178
+ include it. This turns "one return-type decision per file" into "one contract, four includes" —
179
+ the actual remedy for the ad-hoc philosophy.
180
+
181
+ ---
182
+
183
+ ## 4. Test strategy
184
+
185
+ ### Gem rspec additions (lock the CONTRACT, not the current behaviour)
186
+ - **Return-type contract specs**, one per collection, asserting `get_by_name`/`get_by_heading`
187
+ return `Array` (C1, C2) and `get_by_id` return a single (C3, C4). Assert `.first` on a
188
+ multi-match returns the first; assert empty match returns `[]` (not nil) for the collection
189
+ methods — this is the exact shape act-gov `.first`/`.last` and farmers `.first` depend on.
190
+ - **Add-then-find spec** (C5): `coll.add(id:…){…}; expect(coll.get_by_name(label)).to include(the added)`.
191
+ - **`mild:` parity spec** for `get_by_heading` (C2).
192
+ - **Cross-object consistency meta-spec:** a single spec that asserts all four collections agree
193
+ on the return type of `get_by_name` (guards against future drift — the root cause here).
194
+ - Update the 9 spec sites listed under Fix-C1 to `.first` in the SAME commit as the fix.
195
+ - Add `SnakeCamelAccess` characterization spec (P5).
196
+
197
+ ### eco-helpers spec additions
198
+ - Update `OozeRedirect#with_fields` (`ooze_redirect.rb:184-189`) and add a spec proving it
199
+ returns the same shape (Array) whether the field exists or not, against a faithful collection.
200
+ - Regression spec replaying an act-gov-style `get_by_name(...).first` / `.last` through the
201
+ OozeRedirect path.
202
+
203
+ ### Integration coverage (parity harness, training mini-site)
204
+ - On the all-21-fields template mini-org, run the A/B parity harness (memory
205
+ `project_parity_test_plan`): execute one representative ooze case using `.get_by_name(...).first`
206
+ and `.get_by_heading(...).first` against BOTH v2 and GraphQL, assert identical selected field
207
+ IDs and identical emitted mutation input. **Needs live creds** (mini-site API token) — the only
208
+ part of this plan that cannot run in CI without secrets.
209
+ - Replay act-gov `20240130_act_disease_title_update_case.rb` (`.first`+`.last`) and the farmers
210
+ case in `-simulate` mode post-fix to confirm no `.first`-on-single or double-wrap breakage.
211
+
212
+ ---
213
+
214
+ ## 5. Open questions / could not verify
215
+
216
+ 1. **P2/P3 fix status not re-verified.** SYNTHESIS says the IdDiff macro and File/Image/Contractor
217
+ readers were fixed on this branch; I did not re-open those files this pass. Confirm before
218
+ relying on them.
219
+ 2. **Section weight in the GraphQL fragment.** C6/C14 (order-by-weight) only achievable if the
220
+ section fragment actually returns a `weight`/ordering field — not confirmed. If absent, response
221
+ order is the contract and these stay documented gaps.
222
+ 3. **`components.add` write path.** Whether an added field can even be created via the current
223
+ `dataFields.updates`/additions mutation shape without a server-side `buildFromTemplate` ID is a
224
+ separate question (the collection comment at `collection.rb:47` says it "must reference an
225
+ existing server-side ID"). Faithful `add` semantics may be bounded by the mutation API.
226
+ 4. **Full green-suite baseline.** I did not run `bundle exec rspec`; the 9 spec sites are
227
+ identified statically. Run the suite before/after to get exact red counts.
228
+ 5. **`get_by_type` ordering for components (C7).** v2 does not order components by weight either,
229
+ so gem matches — but confirm no consumer relies on component order.
230
+
231
+ ---
232
+
233
+ ## Appendix — files inspected
234
+
235
+ v2 contract: `ecoportal-api-v2/lib/ecoportal/api/v2/page/{components,forces,sections,stages}.rb`,
236
+ `.../page/force/bindings.rb`.
237
+ Gem compat: `lib/.../base/page/data_field/collection.rb`, `.../base/page/section_collection.rb`,
238
+ `.../base/force/{collection,binding_collection,force}.rb`,
239
+ `.../compat/{pages,registers,stage_collection,stage_view}.rb`,
240
+ `.../concerns/{page_compat,data_field_access,snake_camel_access,deprecation}.rb`,
241
+ `.../interface/base_page.rb`.
242
+ Consumers: `multi_org_api/*/config/ooze_cases/*` (grep, ~120 sites),
243
+ `eco-helpers/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb`, `.../ooze_samples/*`.
244
+ Prior findings: `.ai-assistance/code/model_input_mapping/SYNTHESIS.md`.