eco-helpers 3.2.14 → 3.2.16

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/conventions/code-working-tree-protocol.md +176 -0
  3. data/.ai-assistance/scripts/token-logger.js +220 -0
  4. data/.ai-assistance/scripts/token-report.ts +158 -0
  5. data/.ai-assistance/scripts/token-session-start.js +66 -0
  6. data/.ai-assistance/skills/ep-ai-manager/SKILL.md +417 -0
  7. data/.ai-assistance/skills/ruby-scripting/SKILL.md +215 -0
  8. data/.ai-assistance/standards-version.json +10 -0
  9. data/.ai-assistance/token-budget.json +39 -0
  10. data/.claude/settings.json +103 -0
  11. data/.gitignore +2 -0
  12. data/CHANGELOG.md +17 -0
  13. data/CLAUDE.md +83 -0
  14. data/eco-helpers.gemspec +1 -1
  15. data/lib/eco/api/usecases/CLAUDE.md +78 -0
  16. data/lib/eco/api/usecases/default/pages.rb +30 -0
  17. data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
  18. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
  19. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
  20. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
  21. data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
  22. data/lib/eco/api/usecases/graphql/compat.rb +6 -0
  23. data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
  24. data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
  25. data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
  26. data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
  27. data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
  28. data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
  29. data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
  30. data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
  31. data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
  32. data/lib/eco/api/usecases/graphql/samples/pages.rb +7 -0
  33. data/lib/eco/api/usecases/graphql/samples.rb +1 -0
  34. data/lib/eco/api/usecases/graphql.rb +1 -0
  35. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
  36. data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +7 -1
  37. data/lib/eco/version.rb +1 -1
  38. metadata +31 -3
@@ -0,0 +1,103 @@
1
+ {
2
+ "permissions": {
3
+ "defaultMode": "auto",
4
+ "allowedTools": [
5
+ "Read"
6
+ ],
7
+ "permissionRules": [
8
+ {
9
+ "tool": "Write",
10
+ "pattern": "/**",
11
+ "action": "allow"
12
+ },
13
+ {
14
+ "tool": "StrReplace",
15
+ "pattern": "/**",
16
+ "action": "allow"
17
+ }
18
+ ],
19
+ "allow": [
20
+ "WebFetch(domain:anthropic.com)",
21
+ "WebFetch(domain:npmjs.com)",
22
+ "Write(.glaudeignore)",
23
+ "Update(.glaudeignore)",
24
+ "Bash(rm .ai-assistance/bridge/LOCK)",
25
+ "Read(.git/**)",
26
+ "Read(**/rubygems/**)",
27
+ "Write(*.md)",
28
+ "Edit(.ai-assistance/**)",
29
+ "Update(.ai-assistance/**)",
30
+ "Write(.ai-assistance/**)",
31
+ "Write(lib/**)",
32
+ "Write(spec/**)",
33
+ "Bash(git status)",
34
+ "Bash(git diff *)",
35
+ "Bash(git log *)",
36
+ "Bash(git add *)",
37
+ "Bash(git commit *)",
38
+ "PowerShell(git status)",
39
+ "PowerShell(git diff *)",
40
+ "PowerShell(git log *)",
41
+ "PowerShell(git add *)",
42
+ "PowerShell(git commit *)",
43
+ "Bash(npm test)",
44
+ "Bash(npm run lint)",
45
+ "Bash(npm run build)",
46
+ "Bash(vitest *)",
47
+ "Bash(jest *)",
48
+ "PowerShell(npm test)",
49
+ "PowerShell(npm run lint)",
50
+ "PowerShell(npm run build)",
51
+ "Bash(black .)",
52
+ "Bash(pytest)",
53
+ "Bash(python -m unittest)",
54
+ "Bash(ruff check *)",
55
+ "Bash(bundle exec rspec *)",
56
+ "Bash(bundle exec rubocop *)",
57
+ "PowerShell(pytest)",
58
+ "PowerShell(python -m unittest)",
59
+ "PowerShell(ruff check *)",
60
+ "PowerShell(bundle exec rspec *)",
61
+ "PowerShell(bundle exec rubocop *)",
62
+ "Bash(node .ai-assistance/scripts/token-logger.js)",
63
+ "Bash(node .ai-assistance/scripts/token-session-start.js)"
64
+ ],
65
+ "deny": [
66
+ "Read(*.env)",
67
+ "Read(./.env*)",
68
+ "Read(./secrets/**)",
69
+ "Bash(*cat *.env*)",
70
+ "Bash(*grep *.env*)",
71
+ "Bash(printenv*)",
72
+ "Bash(env)",
73
+ "Write(.git/*)",
74
+ "Edit(.git/*)",
75
+ "Bash(git push *)",
76
+ "Bash(rm -rf *)"
77
+ ]
78
+ },
79
+ "hooks": {
80
+ "SessionStart": [
81
+ {
82
+ "matcher": "",
83
+ "hooks": [
84
+ {
85
+ "type": "command",
86
+ "command": "node .ai-assistance/scripts/token-session-start.js 2>/dev/null || true"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "Stop": [
92
+ {
93
+ "matcher": "",
94
+ "hooks": [
95
+ {
96
+ "type": "command",
97
+ "command": "node .ai-assistance/scripts/token-logger.js 2>/dev/null || true"
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ }
103
+ }
data/.gitignore CHANGED
@@ -21,3 +21,5 @@ Gemfile.lock
21
21
  .rspec_status
22
22
  scratch.rb
23
23
  .byebug_history
24
+
25
+ .ai-assistance/local/
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.2.16] - 2026-07-02
6
+
7
+ APIv2→GraphQL cutover — the OozeRedirect compat layer that runs the maintained ooze integrations
8
+ (toocs-coding, cans-upsert, supplier-documents) on GraphQL. Requires ecoportal-api-graphql >= 1.3.9.
9
+
10
+ ### Fixed / Changed
11
+
12
+ - **OozeRedirect slimmed onto the base OozeSamples loop** — removed the `with_each_entry` /
13
+ `update_ooze` / `process_ooze` overrides that duplicated and silently bypassed the base loop
14
+ (KPIs, dedup, queue, `dry_run_feedback`). Fetches go through `ooze → apiv2.pages.get` and saves
15
+ through `update_oozes → update_ooze → apiv2.pages.update`; a captured `submit!`/`sign_off!` rides
16
+ along on the single `updatePage` via `Input::Page::Update.from_model` (reads the `_compat_*` flags).
17
+ - **`dirty?`** also treats a pending `submit!`/`sign_off!` as dirty (so a submit-only page still saves).
18
+ - **`OozeBaseCase#dry_run_feedback`** prints an affirmative `[dry-run] would create/update <ref>` line.
19
+ - **`RegisterUpdateCase#enqueue`** accepts any duck-typed entry (`#dirty?` + `#as_update`), not just
20
+ v2 `Page`/`PageStage` — GraphQL compat pages queue correctly under OozeRedirect.
21
+
5
22
  ## [3.2.15] - 2026-05-xx
6
23
 
7
24
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,83 @@
1
+ # CLAUDE.md — eco-helpers
2
+
3
+ AI agent instructions for this repository.
4
+
5
+ **Cross-cutting architecture context lives in `ecoportal-api-graphql` — see its `CLAUDE.md` and `.claude/` folder for the full dependency map, project history, and shared skills.**
6
+
7
+ ---
8
+
9
+ ## Repository Role
10
+
11
+ `eco-helpers` is the **primary downstream consumer** of the EcoPortal API gem stack. It provides a scripting and automation framework for interacting with EcoPortal — CLI tooling, use-case orchestration, data transformation, and batch operations.
12
+
13
+ **Position in chain:**
14
+ ```
15
+ ecoportal-api
16
+ ecoportal-api-v2
17
+ ecoportal-api-graphql
18
+
19
+ eco-helpers ← THIS REPO
20
+ ```
21
+
22
+ **Remote:** https://gitlab.ecoportal.co.nz/oscar/script_api_helpers.git
23
+
24
+ **Gem dependencies on the stack:**
25
+ - `ecoportal-api ~> 0.10, >= 0.10.14`
26
+ - `ecoportal-api-v2 ~> 3.3, >= 3.3.1`
27
+ - `ecoportal-api-graphql ~> 1.3, >= 1.3.4`
28
+
29
+ ---
30
+
31
+ ## Key Folder Layout
32
+
33
+ ```
34
+ lib/eco/
35
+ api/ API integration layer
36
+ common/ Shared helpers
37
+ microcases/ Fine-grained reusable operations
38
+ organization/ Org-level resources
39
+ session.rb Session management (entry point for scripting)
40
+ usecases.rb Use-case registry
41
+ policies.rb Access policies
42
+ cli/ CLI framework
43
+ cli_default/ Default CLI options, filters, people workflows
44
+ common/ Cross-cutting utilities
45
+ csv/ CSV reading, streaming, splitting
46
+ data/ Data utilities (fuzzy match, hashes, locations, strings, files)
47
+ language/ Logging, curry, auxiliar utilities
48
+ assets/ Static assets (language files etc.)
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Namespace
54
+
55
+ `Eco::` — entirely separate namespace from `Ecoportal::`. Does not reopen upstream gem namespaces.
56
+
57
+ ---
58
+
59
+ ## Key Concerns
60
+
61
+ - This gem is the **backwards-compatibility target** for all upstream gems. When `ecoportal-api-graphql` changes its public interface, check usage here first.
62
+ - `Eco::API::Session` is the main consumer of `Ecoportal::API::GraphQL` — it's the first place to look when checking how GraphQL features are used downstream.
63
+ - The CLI layer (`Eco::CLI`) uses Thor-style commands — changes to API interfaces may silently break CLI workflows if not tested end-to-end.
64
+ - Many operations are batch-oriented with progress logging — error handling and partial-failure behaviour matters.
65
+
66
+ ---
67
+
68
+ ## How to Find GraphQL Usage
69
+
70
+ ```bash
71
+ grep -r "GraphQL\|graphql\|ecoportal-api-graphql" lib/ --include="*.rb" -l
72
+ ```
73
+
74
+ This shows which files directly use the GraphQL gem — useful when assessing impact of upstream changes.
75
+
76
+ ---
77
+
78
+ ## Running Tests
79
+
80
+ ```bash
81
+ bundle install
82
+ bundle exec rspec
83
+ ```
data/eco-helpers.gemspec CHANGED
@@ -42,7 +42,7 @@ Gem::Specification.new do |spec|
42
42
  spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
43
43
  spec.add_dependency 'dotenv', '~> 3'
44
44
  spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.14'
45
- spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.4'
45
+ spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.9'
46
46
  spec.add_dependency 'ecoportal-api-v2', '~> 3.3', '>= 3.3.1'
47
47
  spec.add_dependency 'ed25519', '~> 1.2'
48
48
  spec.add_dependency 'fast_excel', '>= 0.5.0', '< 0.6'
@@ -0,0 +1,78 @@
1
+ # usecases
2
+
3
+ The use-case registry and all built-in case base classes for scripting against EcoPortal.
4
+
5
+ ---
6
+
7
+ ## What a use case is
8
+
9
+ A use case is a self-contained, named, runnable unit of work. It registers itself with
10
+ the CLI framework, receives `session`, `options`, and `usecase` from the runner, and
11
+ executes its `process` (or `process_ooze` / `process_page`) method.
12
+
13
+ ```
14
+ CLI invokes rake → rake finds registered case → UseCase#launch → main() → process()
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Directory structure
20
+
21
+ | Path | What lives there |
22
+ |------|-----------------|
23
+ | `graphql/` | GraphQL-native base cases + samples (see `graphql/CLAUDE.md`) |
24
+ | `ooze_samples/` | APIv2/REST base cases: `OozeBaseCase`, `RegisterUpdateCase` |
25
+ | `ooze_cases/` | Concrete built-in ooze cases (export register, etc.) |
26
+ | `default/` | Built-in people, location, and utility cases |
27
+ | `default_cases/` | Loader and samples for default cases |
28
+ | `samples/` | Misc driver samples |
29
+ | `graphql.rb` | GraphQL use case loader |
30
+ | `ooze_samples.rb` | Ooze/REST use case loader |
31
+ | `default.rb` | Default use case loader |
32
+
33
+ ---
34
+
35
+ ## Adding a new use case
36
+
37
+ 1. Subclass the appropriate base:
38
+
39
+ | Your use case | Inherit from |
40
+ |---|---|
41
+ | Process pages in a register (update workflow) | `Eco::API::UseCases::GraphQL::PageCase` |
42
+ | Process pages org-wide (cross-register, audit) | `Eco::API::UseCases::GraphQL::OrgPageCase` |
43
+ | Custom GraphQL script (export, report, one-off) | `Eco::API::UseCases::GraphQL::Base` |
44
+ | Legacy APIv2 register update | `Eco::API::UseCases::OozeSamples::RegisterUpdateCase` |
45
+
46
+ 2. Set `name` and `type`:
47
+ ```ruby
48
+ name 'my-case-name' # CLI identifier: called with -my-case-name
49
+ type :other # :people | :contractors | :other
50
+ ```
51
+
52
+ 3. Override the entry point (`process_page`, `process`, or `process_ooze`).
53
+
54
+ 4. Register in the org's `config/cli.rb`:
55
+ ```ruby
56
+ cases.add('-my-case-name', :other, 'Description')
57
+ ```
58
+
59
+ ---
60
+
61
+ ## How cases are launched
62
+
63
+ `Eco::API::UseCases::UseCase#launch` calls `callback.call(*uio.params)` where the
64
+ callback is bound to `method(:main)`. Before launch, `@session` and `@options` are
65
+ injected into the instance — subclasses access them via the `attr_reader` in `CaseEnv`.
66
+
67
+ The `:other` type passes `(session, options, usecase)` positionally to `main`.
68
+ For `GraphQL::Base` subclasses the signature is `main(*_args)` — `session` and
69
+ `options` are already available via the helpers module before `main` is called.
70
+
71
+ ---
72
+
73
+ ## Related
74
+
75
+ - `graphql/CLAUDE.md` — GraphQL case hierarchy, PageCase/OrgPageCase
76
+ - `ooze_samples/` — legacy v2 cases (RegisterUpdateCase, OozeBaseCase)
77
+ - `eco-helpers/CLAUDE.md` — top-level gem context
78
+ - `ecoportal-api-graphql` — upstream gem providing `SearchConf`, `Compat::Pages`, etc.
@@ -0,0 +1,30 @@
1
+ module Eco
2
+ module API
3
+ class UseCases
4
+ class Default
5
+ # Namespace for CLI-integrated page use cases.
6
+ # These are concrete, fully functional cases exposed to all org environments.
7
+ #
8
+ # == Convention
9
+ # Cases here should:
10
+ # - Inherit from Eco::API::UseCases::GraphQL::Samples::Pages::Page::Base
11
+ # (or OrgPage::Base for org-wide operations)
12
+ # - Be registered in the org's config/cli.rb
13
+ # - Be as org-agnostic as possible (accept register_id, filters via options)
14
+ #
15
+ # == Adding a new default page case
16
+ # 1. Create the file here: default/pages/my_case.rb
17
+ # 2. Inherit from the appropriate pages sample base class
18
+ # 3. Require it below
19
+ # 4. Wire CLI registration in usecases/default_cases/samples.rb or the
20
+ # org's config/cli.rb
21
+ #
22
+ # == Currently
23
+ # No default page cases yet — add them here as common patterns emerge
24
+ # across org implementations.
25
+ module Pages
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,120 @@
1
+ # usecases/graphql
2
+
3
+ GraphQL-native use case base classes and helpers. All cases here work directly with
4
+ `ecoportal-api-graphql` — no v2 REST layer, no ooze objects.
5
+
6
+ ---
7
+
8
+ ## Class hierarchy
9
+
10
+ ```
11
+ Eco::API::Common::Loaders::UseCase (registration + launch)
12
+
13
+ Eco::API::UseCases::GraphQL::Base ← universal GraphQL env
14
+ ├── GraphQL::Samples::Pages::Page::Base ← register-scoped pages
15
+ │ ├── GraphQL::Samples::Pages::OrgPage::Base ← org-wide pages
16
+ │ └── your subclass (process_page, search_conf)
17
+ └── your subclass directly (custom scripts: exports, reports, one-offs)
18
+ ```
19
+
20
+ Samples live under `samples/pages/` — NOT in the `graphql/` root. The root only
21
+ has `base.rb`, `helpers.rb`, `utils.rb`, and `samples.rb`.
22
+
23
+ ---
24
+
25
+ ## Base — `graphql/base.rb`
26
+
27
+ Universal GraphQL environment. Provides `graphql`, `session`, `options`, `config`,
28
+ `simulate?`, `log`, `backup` via `Helpers::Base` (see `helpers/CLAUDE.md`).
29
+
30
+ Override `process` to write your script:
31
+ ```ruby
32
+ class MyCase < Eco::API::UseCases::GraphQL::Base
33
+ name 'my-case'
34
+ def process
35
+ graphql.currentOrganization.contractorEntities.each { |c| puts c.name }
36
+ end
37
+ end
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Pages — `samples/pages/`
43
+
44
+ Page processing base cases. Follow the hierarchy: `page/base` → `org_page/base`.
45
+
46
+ ### `samples/pages/page/base.rb` — `Samples::Pages::Page::Base`
47
+
48
+ For **register-scoped** page update workflows.
49
+
50
+ **Class methods:** `register_id 'REG_ID'`, `batch_size 50` (default)
51
+
52
+ **Override points:**
53
+ - `process_page(page)` — **required** — transformation for one page
54
+ - `search_conf` — optional — call `super` to keep register scope, then add filters
55
+
56
+ **Protected helpers:** `update_page`, `skip(reason)`, `each_page`
57
+
58
+ **KPI readers:** `total_pages`, `processed_pages`, `updated_pages`, `skipped_pages`, `failed_pages`
59
+
60
+ **DSL (via `samples/pages/page/dsl.rb`):** `sc`, `in_register`, `state_is`, `external_id_eq`, `updated_since`
61
+
62
+ ```ruby
63
+ class Custom::UseCase::UpdateStatus < Eco::API::UseCases::GraphQL::Samples::Pages::Page::Base
64
+ name 'update-status'
65
+ register_id 'REG_ABC'
66
+
67
+ def search_conf
68
+ super.filter(state_is(:active))
69
+ end
70
+
71
+ def process_page(page)
72
+ page.name = page.name.upcase
73
+ update_page(page)
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### `samples/pages/org_page/base.rb` — `Samples::Pages::OrgPage::Base`
79
+
80
+ Inherits `Page::Base`. `search_conf` starts empty (org-wide, no register scope).
81
+ Use for: archive sweeps, cross-register audits, bulk org operations.
82
+
83
+ ---
84
+
85
+ ## Samples — `graphql/samples/`
86
+
87
+ Built-in ready-to-use case implementations:
88
+ - `samples/location.rb` — location structure management cases
89
+ - `samples/contractors.rb` — contractor entity cases
90
+
91
+ See `samples/CLAUDE.md` for details.
92
+
93
+ ---
94
+
95
+ ## Helpers — `graphql/helpers/`
96
+
97
+ Mixins providing domain-specific access patterns. See `helpers/CLAUDE.md`.
98
+
99
+ ---
100
+
101
+ ## Loader order in `graphql.rb`
102
+
103
+ ```ruby
104
+ require 'graphql/helpers' # environment mixins (graphql, session, simulate? etc.)
105
+ require 'graphql/utils' # utility modules (SFTP etc.)
106
+ require 'graphql/base' # GraphQL::Base — universal foundation
107
+ require 'graphql/samples' # sample cases: location, contractors, pages, ...
108
+ # └─ graphql/samples/pages.rb
109
+ # └─ pages/page.rb → page/dsl.rb, page/base.rb
110
+ # └─ pages/org_page.rb → org_page/dsl.rb, org_page/base.rb
111
+ ```
112
+
113
+ Page base cases are in `samples/pages/` — NOT in the `graphql/` root.
114
+ Custom org cases are NOT loaded here — they live in the implementation repo.
115
+
116
+ ## default/pages/
117
+
118
+ CLI-integrated page use cases go in `default/pages/` (mirroring `default/locations/`
119
+ and `default/people/`). Currently empty — add cases there when a pattern is common
120
+ enough to expose to all org environments. See `default/pages.rb` for the convention.
@@ -0,0 +1,22 @@
1
+ module Eco::API::UseCases::GraphQL::Compat::OozeRedirect
2
+ # Array subclass that calls the field's setter when elements are appended,
3
+ # ensuring GraphQL dirty-tracking fires on `fld.people_ids << value`.
4
+ class DirtyArray < Array
5
+ def initialize(field, data)
6
+ @field = field
7
+ super(Array(data))
8
+ end
9
+
10
+ def <<(value)
11
+ super
12
+ @field.people_ids = to_a
13
+ self
14
+ end
15
+
16
+ def push(*values)
17
+ super
18
+ @field.people_ids = to_a
19
+ self
20
+ end
21
+ end
22
+ end