eco-helpers 3.2.16 → 3.2.22
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/CHANGELOG.md +190 -0
- data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
- data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
- data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
- data/lib/eco/api/usecases/graphql/compat.rb +5 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
- data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
- data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
- data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
- data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
- data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
- data/lib/eco/api/usecases/graphql/samples/pages.rb +2 -0
- data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +6 -2
- data/lib/eco/version.rb +1 -1
- metadata +34 -25
- data/.ai-assistance/conventions/code-working-tree-protocol.md +0 -176
- data/.ai-assistance/scripts/token-logger.js +0 -220
- data/.ai-assistance/scripts/token-report.ts +0 -158
- data/.ai-assistance/scripts/token-session-start.js +0 -66
- data/.ai-assistance/skills/ep-ai-manager/SKILL.md +0 -417
- data/.ai-assistance/skills/ruby-scripting/SKILL.md +0 -215
- data/.ai-assistance/standards-version.json +0 -10
- data/.ai-assistance/token-budget.json +0 -39
- data/.claude/settings.json +0 -103
- data/.gitignore +0 -25
- data/.idea/.gitignore +0 -10
- data/.markdownlint.json +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -103
- data/.ruby-version +0 -1
- data/.yardopts +0 -10
- data/CLAUDE.md +0 -83
- data/Gemfile +0 -8
- data/Rakefile +0 -38
- data/eco-helpers.gemspec +0 -63
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
module Deploy
|
|
3
|
+
# Sync-readiness monitoring.
|
|
4
|
+
#
|
|
5
|
+
# Given a register subset (its entries) and the register's ACTIVE template, report which entries
|
|
6
|
+
# CAN sync vs which cannot — where "can sync" means: every field the template marks REQUIRED is
|
|
7
|
+
# present on the entry with a non-empty value AND typed correctly (the entry field's type matches
|
|
8
|
+
# the template field's type). This is the pre-flight an integration runs before pushing entries
|
|
9
|
+
# downstream, and the drift-detection the customer-facing integrations mandate needs.
|
|
10
|
+
#
|
|
11
|
+
# Operates on raw docs (template doc + entry docs) so it is session-less and offline-runnable:
|
|
12
|
+
# * template doc: the JSON the gem returns for a template (stages/sections/dataFields).
|
|
13
|
+
# * entry docs: each register entry as { 'id' =>, 'fields' => [ {id/label/type/value}, ... ] }
|
|
14
|
+
# (or anything exposing the same shape). A field is matched to a template field by id first,
|
|
15
|
+
# then by label — mirroring how the pairing engine falls back.
|
|
16
|
+
#
|
|
17
|
+
# monitor = SyncReadiness.new(template_doc: tpl, entries: [e1, e2, ...])
|
|
18
|
+
# monitor.ready # => [EntryReadiness(ready: true), ...]
|
|
19
|
+
# monitor.not_ready # => [EntryReadiness(ready: false, ...), ...]
|
|
20
|
+
# monitor.summary # => { total:, ready:, not_ready: }
|
|
21
|
+
# monitor.report # => human summary
|
|
22
|
+
class SyncReadiness
|
|
23
|
+
# Per-entry readiness verdict.
|
|
24
|
+
EntryReadiness = Struct.new(:entry_id, :ready, :missing_required, :type_mismatches,
|
|
25
|
+
keyword_init: true) do
|
|
26
|
+
def ready?
|
|
27
|
+
ready ? true : false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reasons
|
|
31
|
+
out = Array(missing_required).map { |m| "missing required '#{m}'" }
|
|
32
|
+
Array(type_mismatches).each do |m|
|
|
33
|
+
out << "type mismatch '#{m[:label]}' (want #{m[:expected]}, got #{m[:actual]})"
|
|
34
|
+
end
|
|
35
|
+
out
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{ entry_id: entry_id, ready: ready?, missing_required: Array(missing_required),
|
|
40
|
+
type_mismatches: Array(type_mismatches) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_reader :entries
|
|
45
|
+
|
|
46
|
+
# @param template_doc [Hash] the active template doc.
|
|
47
|
+
# @param entries [Array<Hash,#id>] register entry docs to assess.
|
|
48
|
+
def initialize(template_doc:, entries:)
|
|
49
|
+
@template_doc = template_doc || {}
|
|
50
|
+
@entries = Array(entries)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def verdicts
|
|
54
|
+
@verdicts ||= @entries.map { |entry| assess(entry) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ready
|
|
58
|
+
verdicts.select(&:ready?)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def not_ready
|
|
62
|
+
verdicts.reject(&:ready?)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def summary
|
|
66
|
+
{ total: verdicts.size, ready: ready.size, not_ready: not_ready.size }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{ summary: summary, entries: verdicts.map(&:to_h) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def report
|
|
74
|
+
lines = ["SYNC READINESS: #{summary[:ready]}/#{summary[:total]} entries ready"]
|
|
75
|
+
not_ready.each do |v|
|
|
76
|
+
lines << " * entry #{v.entry_id}: NOT READY"
|
|
77
|
+
v.reasons.each { |r| lines << " - #{r}" }
|
|
78
|
+
end
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# The template fields, indexed by id and by (downcased) label, carrying type + required.
|
|
85
|
+
def template_fields
|
|
86
|
+
@template_fields ||= begin
|
|
87
|
+
out = { by_id: {}, by_label: {} }
|
|
88
|
+
each_template_field do |f|
|
|
89
|
+
spec = { id: f['id'], label: f['label'], type: field_type(f), required: !!f['required'] }
|
|
90
|
+
out[:by_id][f['id']] = spec if f['id']
|
|
91
|
+
out[:by_label][label_key(f['label'])] = spec if f['label']
|
|
92
|
+
end
|
|
93
|
+
out
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def required_fields
|
|
98
|
+
@required_fields ||= template_fields[:by_id].values.select { |s| s[:required] }.
|
|
99
|
+
+(template_fields[:by_label].values.select { |s| s[:required] }).
|
|
100
|
+
uniq { |s| s[:id] || s[:label] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def assess(entry)
|
|
104
|
+
entry_fields = index_entry_fields(entry)
|
|
105
|
+
missing = []
|
|
106
|
+
mismatches = []
|
|
107
|
+
|
|
108
|
+
required_fields.each do |spec|
|
|
109
|
+
efield = entry_fields[:by_id][spec[:id]] || entry_fields[:by_label][label_key(spec[:label])]
|
|
110
|
+
if efield.nil? || value_empty?(efield[:value])
|
|
111
|
+
missing << spec[:label]
|
|
112
|
+
elsif spec[:type] && efield[:type] && spec[:type] != efield[:type]
|
|
113
|
+
mismatches << { label: spec[:label], expected: spec[:type], actual: efield[:type] }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
EntryReadiness.new(
|
|
118
|
+
entry_id: entry_id(entry),
|
|
119
|
+
ready: missing.empty? && mismatches.empty?,
|
|
120
|
+
missing_required: missing,
|
|
121
|
+
type_mismatches: mismatches
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def index_entry_fields(entry)
|
|
126
|
+
out = { by_id: {}, by_label: {} }
|
|
127
|
+
entry_field_docs(entry).each do |f|
|
|
128
|
+
spec = { id: f['id'] || f[:id], label: f['label'] || f[:label],
|
|
129
|
+
type: field_type(f), value: f['value'] || f[:value] }
|
|
130
|
+
out[:by_id][spec[:id]] = spec if spec[:id]
|
|
131
|
+
out[:by_label][label_key(spec[:label])] = spec if spec[:label]
|
|
132
|
+
end
|
|
133
|
+
out
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def entry_field_docs(entry)
|
|
137
|
+
return Array(entry['fields'] || entry[:fields]) if entry.is_a?(Hash)
|
|
138
|
+
return entry.components.to_a if entry.respond_to?(:components)
|
|
139
|
+
|
|
140
|
+
[]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def entry_id(entry)
|
|
144
|
+
return entry['id'] || entry[:id] if entry.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
entry.respond_to?(:id) ? entry.id : nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def each_template_field(&block)
|
|
150
|
+
Array(@template_doc['stages']).each do |st|
|
|
151
|
+
Array(st['sections']).each do |sec|
|
|
152
|
+
fields_of(sec).each(&block)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def fields_of(section)
|
|
158
|
+
Array(section['dataFields']) + Array(section['leftDataFields']) + Array(section['rightDataFields'])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def field_type(field)
|
|
162
|
+
field['__typename'] || field['type'] || field[:type]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def label_key(label)
|
|
166
|
+
label.to_s.strip.downcase
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def value_empty?(value)
|
|
170
|
+
return true if value.nil?
|
|
171
|
+
return value.strip.empty? if value.is_a?(String)
|
|
172
|
+
return value.empty? if value.respond_to?(:empty?)
|
|
173
|
+
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
module Deploy
|
|
3
|
+
# Pluggable post-deploy verification interface.
|
|
4
|
+
#
|
|
5
|
+
# The deploy loop wants to run the freshly-deployed template through a check framework (ideally the
|
|
6
|
+
# `ecoportal-qa` gem's check runner). But `ecoportal-qa` is NOT a hard dependency of eco-helpers,
|
|
7
|
+
# so we do not `require` it here. Instead the loop calls a small `Verifier` interface; a concrete
|
|
8
|
+
# verifier is injected. When `ecoportal-qa` IS on the load path, `QaVerifier` wires it in; when it
|
|
9
|
+
# isn't, `NullVerifier` (always-pass, records that qa was unavailable) keeps the loop runnable
|
|
10
|
+
# offline.
|
|
11
|
+
#
|
|
12
|
+
# A verifier receives the post-deploy template doc and returns a `Verdict`.
|
|
13
|
+
#
|
|
14
|
+
# verdict = Verifier.default.verify(post_deploy_doc)
|
|
15
|
+
# verdict.pass? # => true/false
|
|
16
|
+
# verdict.findings # => [ {severity:, message:}, ... ]
|
|
17
|
+
class Verifier
|
|
18
|
+
# A verification verdict. `pass?` is the gate; `findings` carries per-check detail.
|
|
19
|
+
Verdict = Struct.new(:pass, :findings, :verifier, keyword_init: true) do
|
|
20
|
+
def pass?
|
|
21
|
+
pass ? true : false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def failures
|
|
25
|
+
Array(findings).select { |f| %i[error fail].include?((f[:severity] || f['severity'])&.to_sym) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{ pass: pass?, verifier: verifier, findings: Array(findings) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def report
|
|
33
|
+
head = "VERIFY (#{verifier}): #{pass? ? 'PASS' : 'FAIL'}"
|
|
34
|
+
return head if Array(findings).empty?
|
|
35
|
+
|
|
36
|
+
([head] + Array(findings).map do |f|
|
|
37
|
+
" - [#{f[:severity] || f['severity']}] #{f[:message] || f['message']}"
|
|
38
|
+
end).join("\n")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The verifier the loop uses when none is injected: qa if available, else the null verifier.
|
|
43
|
+
def self.default
|
|
44
|
+
qa_available? ? QaVerifier.new : NullVerifier.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Is the ecoportal-qa check framework loadable?
|
|
48
|
+
def self.qa_available?
|
|
49
|
+
return @qa_available unless @qa_available.nil?
|
|
50
|
+
|
|
51
|
+
@qa_available = begin
|
|
52
|
+
require 'ecoportal/qa'
|
|
53
|
+
defined?(::Ecoportal::QA) ? true : false
|
|
54
|
+
rescue LoadError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reset the memoised availability probe (used by specs that stub the load path).
|
|
60
|
+
def self.reset_qa_probe!
|
|
61
|
+
@qa_available = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Subclass / duck-type contract: return a Verdict for the given template doc.
|
|
65
|
+
def verify(_template_doc)
|
|
66
|
+
raise NotImplementedError, "#{self.class} must implement #verify(template_doc)"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Always-pass verifier used when no real check framework is wired in. It is honest about it:
|
|
70
|
+
# the verdict records that qa was unavailable so a report never falsely claims "qa verified".
|
|
71
|
+
class NullVerifier < Verifier
|
|
72
|
+
def verify(_template_doc)
|
|
73
|
+
Verdict.new(
|
|
74
|
+
pass: true,
|
|
75
|
+
verifier: 'null',
|
|
76
|
+
findings: [{ severity: :info, message: 'ecoportal-qa not available; no checks run' }]
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Adapter onto the ecoportal-qa check runner. Kept dependency-free at load time — it only touches
|
|
82
|
+
# `Ecoportal::QA` inside #verify, so requiring this file never forces the qa gem. The exact qa
|
|
83
|
+
# runner API is intentionally accessed through a single seam (`run_qa`) so it is easy to correct
|
|
84
|
+
# once the qa public surface stabilises (see TODO).
|
|
85
|
+
#
|
|
86
|
+
# TODO(qa-integration): the qa check-runner public API is still moving (see the qa repo's
|
|
87
|
+
# PHASE3-SCOPE.md). This adapter assumes a `check_set.run(doc)`-style entry returning objects
|
|
88
|
+
# responding to `#pass?` and `#findings`. Confirm and adjust `run_qa` when qa is pinned as a dep.
|
|
89
|
+
class QaVerifier < Verifier
|
|
90
|
+
def initialize(check_set: nil)
|
|
91
|
+
super()
|
|
92
|
+
@check_set = check_set
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def verify(template_doc)
|
|
96
|
+
result = run_qa(template_doc)
|
|
97
|
+
Verdict.new(pass: result_pass?(result), verifier: 'ecoportal-qa', findings: result_findings(result))
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
Verdict.new(pass: false, verifier: 'ecoportal-qa',
|
|
100
|
+
findings: [{ severity: :error, message: "qa verification raised: #{e.message}" }])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# SINGLE integration seam — the one place that knows the qa runner shape.
|
|
106
|
+
def run_qa(template_doc)
|
|
107
|
+
set = @check_set || default_check_set
|
|
108
|
+
set.run(template_doc)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def default_check_set
|
|
112
|
+
raise NotImplementedError, 'no qa check_set injected and no default wired' unless defined?(::Ecoportal::QA)
|
|
113
|
+
|
|
114
|
+
# Placeholder: the concrete constructor is confirmed once qa is a pinned dependency.
|
|
115
|
+
if ::Ecoportal::QA.respond_to?(:default_check_set)
|
|
116
|
+
::Ecoportal::QA.default_check_set
|
|
117
|
+
else
|
|
118
|
+
(raise NotImplementedError,
|
|
119
|
+
'qa default check set not available')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def result_pass?(result)
|
|
124
|
+
return result.pass? if result.respond_to?(:pass?)
|
|
125
|
+
|
|
126
|
+
Array(result).all? { |r| r.respond_to?(:pass?) ? r.pass? : true }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def result_findings(result)
|
|
130
|
+
source = result.respond_to?(:findings) ? result.findings : result
|
|
131
|
+
Array(source).map do |f|
|
|
132
|
+
next f if f.is_a?(Hash)
|
|
133
|
+
|
|
134
|
+
{ severity: (f.respond_to?(:severity) ? f.severity : :info),
|
|
135
|
+
message: (f.respond_to?(:message) ? f.message : f.to_s) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
# Phase 5: deploy → verify → monitor loop for template (workflow) changes.
|
|
3
|
+
#
|
|
4
|
+
# Consumes a gem `Ecoportal::API::GraphQL::Diff::Deploy` command batch (from
|
|
5
|
+
# `Diff::Deploy.from_versions(...)`), dry-run applies it (live behind an explicit `commit:` flag),
|
|
6
|
+
# confirms the applied delta matches the intended delta via a pre/post self-version diff
|
|
7
|
+
# (`Diff::VersionDiff`), verifies the result through a pluggable `Verifier` (ecoportal-qa when
|
|
8
|
+
# present), and reports register sync-readiness against the deployed template.
|
|
9
|
+
#
|
|
10
|
+
# All components are session-less and offline-runnable against fixtures — the live seams (apply /
|
|
11
|
+
# re-read) are injected. See each file's header. Dry-run is the DEFAULT.
|
|
12
|
+
module Deploy
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative 'deploy/applier'
|
|
17
|
+
require_relative 'deploy/recording_executor'
|
|
18
|
+
require_relative 'deploy/drift_report'
|
|
19
|
+
require_relative 'deploy/verifier'
|
|
20
|
+
require_relative 'deploy/sync_readiness'
|
|
21
|
+
require_relative 'deploy/loop'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# Template (workflow) build-from-scratch + (later) diff-and-update samples.
|
|
3
|
+
# See ecoportal-api-graphql/.ai-assistance/projects/template-maintenance/.
|
|
4
|
+
module Template
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative 'template/command_emitter'
|
|
9
|
+
require_relative 'template/base'
|
|
10
|
+
require_relative 'template/csv_build'
|
|
11
|
+
require_relative 'template/deploy'
|
|
@@ -54,7 +54,10 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
|
|
|
54
54
|
return unless (pending = queue_shift(ooze_id))
|
|
55
55
|
|
|
56
56
|
update_ooze(pending).tap do |result|
|
|
57
|
-
|
|
57
|
+
# Duck-type, not is_a?: the GraphQL Compat::Response responds to success?/status but is
|
|
58
|
+
# NOT an Ecoportal::API::Common::Response, so is_a? silently skipped every GraphQL update
|
|
59
|
+
# (updated/failed stuck at 0). false/nil returns (dry-run / no-op) still fall through.
|
|
60
|
+
if result.respond_to?(:success?)
|
|
58
61
|
if result.success?
|
|
59
62
|
@updated_oozes += 1
|
|
60
63
|
else
|
|
@@ -188,7 +191,8 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
|
|
|
188
191
|
def update_oozes(batched_oozes = batch_queue)
|
|
189
192
|
batched_oozes.each do |ooze|
|
|
190
193
|
update_ooze(ooze).tap do |result|
|
|
191
|
-
|
|
194
|
+
# Duck-type, not is_a? — see #before_loading_new_target (GraphQL Compat::Response).
|
|
195
|
+
if result.respond_to?(:success?)
|
|
192
196
|
if result.success?
|
|
193
197
|
@updated_oozes += 1
|
|
194
198
|
else
|
data/lib/eco/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eco-helpers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.2.
|
|
4
|
+
version: 3.2.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oscar Segura
|
|
@@ -262,7 +262,7 @@ dependencies:
|
|
|
262
262
|
version: '1.3'
|
|
263
263
|
- - ">="
|
|
264
264
|
- !ruby/object:Gem::Version
|
|
265
|
-
version: 1.3.
|
|
265
|
+
version: 1.3.14
|
|
266
266
|
type: :runtime
|
|
267
267
|
prerelease: false
|
|
268
268
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -272,7 +272,7 @@ dependencies:
|
|
|
272
272
|
version: '1.3'
|
|
273
273
|
- - ">="
|
|
274
274
|
- !ruby/object:Gem::Version
|
|
275
|
-
version: 1.3.
|
|
275
|
+
version: 1.3.14
|
|
276
276
|
- !ruby/object:Gem::Dependency
|
|
277
277
|
name: ecoportal-api-v2
|
|
278
278
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -282,7 +282,7 @@ dependencies:
|
|
|
282
282
|
version: '3.3'
|
|
283
283
|
- - ">="
|
|
284
284
|
- !ruby/object:Gem::Version
|
|
285
|
-
version: 3.3.
|
|
285
|
+
version: 3.3.3
|
|
286
286
|
type: :runtime
|
|
287
287
|
prerelease: false
|
|
288
288
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -292,7 +292,7 @@ dependencies:
|
|
|
292
292
|
version: '3.3'
|
|
293
293
|
- - ">="
|
|
294
294
|
- !ruby/object:Gem::Version
|
|
295
|
-
version: 3.3.
|
|
295
|
+
version: 3.3.3
|
|
296
296
|
- !ruby/object:Gem::Dependency
|
|
297
297
|
name: ed25519
|
|
298
298
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -541,29 +541,9 @@ executables: []
|
|
|
541
541
|
extensions: []
|
|
542
542
|
extra_rdoc_files: []
|
|
543
543
|
files:
|
|
544
|
-
- ".ai-assistance/conventions/code-working-tree-protocol.md"
|
|
545
|
-
- ".ai-assistance/scripts/token-logger.js"
|
|
546
|
-
- ".ai-assistance/scripts/token-report.ts"
|
|
547
|
-
- ".ai-assistance/scripts/token-session-start.js"
|
|
548
|
-
- ".ai-assistance/skills/ep-ai-manager/SKILL.md"
|
|
549
|
-
- ".ai-assistance/skills/ruby-scripting/SKILL.md"
|
|
550
|
-
- ".ai-assistance/standards-version.json"
|
|
551
|
-
- ".ai-assistance/token-budget.json"
|
|
552
|
-
- ".claude/settings.json"
|
|
553
|
-
- ".gitignore"
|
|
554
|
-
- ".idea/.gitignore"
|
|
555
|
-
- ".markdownlint.json"
|
|
556
|
-
- ".rspec"
|
|
557
|
-
- ".rubocop.yml"
|
|
558
|
-
- ".ruby-version"
|
|
559
|
-
- ".yardopts"
|
|
560
544
|
- CHANGELOG.md
|
|
561
|
-
- CLAUDE.md
|
|
562
|
-
- Gemfile
|
|
563
545
|
- LICENSE
|
|
564
546
|
- README.md
|
|
565
|
-
- Rakefile
|
|
566
|
-
- eco-helpers.gemspec
|
|
567
547
|
- lib/eco-helpers.rb
|
|
568
548
|
- lib/eco/api.rb
|
|
569
549
|
- lib/eco/api/common.rb
|
|
@@ -844,6 +824,9 @@ files:
|
|
|
844
824
|
- lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb
|
|
845
825
|
- lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb
|
|
846
826
|
- lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb
|
|
827
|
+
- lib/eco/api/usecases/graphql/compat/parity/comparison.rb
|
|
828
|
+
- lib/eco/api/usecases/graphql/compat/parity/harness.rb
|
|
829
|
+
- lib/eco/api/usecases/graphql/compat/parity/run_result.rb
|
|
847
830
|
- lib/eco/api/usecases/graphql/helpers.rb
|
|
848
831
|
- lib/eco/api/usecases/graphql/helpers/CLAUDE.md
|
|
849
832
|
- lib/eco/api/usecases/graphql/helpers/base.rb
|
|
@@ -877,6 +860,14 @@ files:
|
|
|
877
860
|
- lib/eco/api/usecases/graphql/helpers/location/tags_remap.rb
|
|
878
861
|
- lib/eco/api/usecases/graphql/helpers/location/tags_remap/tags_map.rb
|
|
879
862
|
- lib/eco/api/usecases/graphql/helpers/location/tags_remap/tags_set.rb
|
|
863
|
+
- lib/eco/api/usecases/graphql/helpers/pages.rb
|
|
864
|
+
- lib/eco/api/usecases/graphql/helpers/pages/copying.rb
|
|
865
|
+
- lib/eco/api/usecases/graphql/helpers/pages/creatable.rb
|
|
866
|
+
- lib/eco/api/usecases/graphql/helpers/pages/filters.rb
|
|
867
|
+
- lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb
|
|
868
|
+
- lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb
|
|
869
|
+
- lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb
|
|
870
|
+
- lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb
|
|
880
871
|
- lib/eco/api/usecases/graphql/samples.rb
|
|
881
872
|
- lib/eco/api/usecases/graphql/samples/CLAUDE.md
|
|
882
873
|
- lib/eco/api/usecases/graphql/samples/contractors.rb
|
|
@@ -913,6 +904,24 @@ files:
|
|
|
913
904
|
- lib/eco/api/usecases/graphql/samples/pages/page.rb
|
|
914
905
|
- lib/eco/api/usecases/graphql/samples/pages/page/base.rb
|
|
915
906
|
- lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb
|
|
907
|
+
- lib/eco/api/usecases/graphql/samples/pages/register.rb
|
|
908
|
+
- lib/eco/api/usecases/graphql/samples/pages/register/base.rb
|
|
909
|
+
- lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb
|
|
910
|
+
- lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb
|
|
911
|
+
- lib/eco/api/usecases/graphql/samples/pages/template.rb
|
|
912
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/base.rb
|
|
913
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb
|
|
914
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb
|
|
915
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb
|
|
916
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb
|
|
917
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb
|
|
918
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb
|
|
919
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb
|
|
920
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb
|
|
921
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb
|
|
922
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb
|
|
923
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb
|
|
924
|
+
- lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb
|
|
916
925
|
- lib/eco/api/usecases/graphql/utils.rb
|
|
917
926
|
- lib/eco/api/usecases/graphql/utils/sftp.rb
|
|
918
927
|
- lib/eco/api/usecases/lib.rb
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# Code Working Tree Protocol
|
|
2
|
-
|
|
3
|
-
When Claude Code needs to make changes to files **outside** `bridge/inbox/`, it must
|
|
4
|
-
follow this protocol. This prevents Code's changes from mixing with in-progress CoWork
|
|
5
|
-
edits and ensures a clean, traceable commit history.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## When this applies
|
|
10
|
-
|
|
11
|
-
Any time Code intends to modify files in the working tree that are not bridge task files
|
|
12
|
-
(i.e., not `.ai-assistance/bridge/inbox/` or `.ai-assistance/bridge/outbox/`).
|
|
13
|
-
|
|
14
|
-
This includes: editing source files, updating documentation, changing scripts,
|
|
15
|
-
modifying capabilities files, etc.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Protocol
|
|
20
|
-
|
|
21
|
-
### 0. Check for a lock
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
cat .ai-assistance/bridge/LOCK 2>/dev/null || echo "NO_LOCK"
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
- **No lock:** proceed to step 1
|
|
28
|
-
- **Lock exists, EXPIRES is in the future:** stop. Tell the user:
|
|
29
|
-
> "Working tree is locked by [AGENT] ([USER]) since [ACQUIRED], working on: [INTENT].
|
|
30
|
-
> Expires at [EXPIRES]. Please wait or check if the other session is still active."
|
|
31
|
-
- **Lock exists, EXPIRES is in the past:** stale lock — safe to overwrite, proceed to step 1
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
### 1. Acquire the lock
|
|
36
|
-
|
|
37
|
-
Write `.ai-assistance/bridge/LOCK` with full watermark:
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
AGENT: code
|
|
41
|
-
USER: [git config user.name, lowercased]
|
|
42
|
-
ACQUIRED: [ISO 8601 now]
|
|
43
|
-
EXPIRES: [ISO 8601 now + 30 minutes]
|
|
44
|
-
INTENT: [one sentence — what you are about to change and why]
|
|
45
|
-
FILES: [comma-separated list of files you plan to modify]
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Example:
|
|
49
|
-
```
|
|
50
|
-
AGENT: code
|
|
51
|
-
USER: oscar
|
|
52
|
-
ACQUIRED: 2026-06-04T10:00:00Z
|
|
53
|
-
EXPIRES: 2026-06-04T10:30:00Z
|
|
54
|
-
INTENT: Update gitlab-mcp.md with new PAT scopes and rotation info
|
|
55
|
-
FILES: .ai-assistance/integrations/gitlab-mcp.md
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
### 2. Check for unstaged changes that overlap with your planned files
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
git status --short
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
If the working tree is clean, skip to step 3.
|
|
67
|
-
|
|
68
|
-
If there are unstaged/staged changes, compare them against the files listed in your LOCK:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
git diff --name-only HEAD
|
|
72
|
-
git diff --cached --name-only
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
- **No overlap with your FILES:** proceed — the changes are unrelated and won't pollute history
|
|
76
|
-
- **Overlap with one or more of your FILES:** commit the unstaged changes first.
|
|
77
|
-
Derive the commit message by running `git diff HEAD` on the overlapping files and
|
|
78
|
-
writing a short imperative summary of what actually changed — do not use a generic
|
|
79
|
-
message. Format: `wip: <what changed, e.g. "rename .claude to .ai-assistance across scripts">`
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
git add -A
|
|
83
|
-
git commit -m "wip: <derived from actual diff>"
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
This keeps Code's subsequent commit clean and ensures both sets of changes build
|
|
87
|
-
on the correct base. On a feature branch, `wip:` commits are fine — squash before MR.
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
### 3. Apply your changes
|
|
92
|
-
|
|
93
|
-
Make the intended file edits. Stay within the scope declared in INTENT and FILES
|
|
94
|
-
when you acquired the lock. If scope expands, update the LOCK file before proceeding.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
### 4. Commit your changes
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
git add -A
|
|
102
|
-
git commit -m "[descriptive message — what Code changed and why]"
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Commit message should be specific enough that a teammate can understand the change
|
|
106
|
-
without reading the diff. Example:
|
|
107
|
-
```
|
|
108
|
-
docs: update gitlab-mcp.md scopes and rotation info for new PAT (April 2027 expiry)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Commit authorship — developer only by default:**
|
|
112
|
-
|
|
113
|
-
Commits are authored by the developer alone (git's `user.name` / `user.email` config).
|
|
114
|
-
Do NOT add `Co-Authored-By: Claude ...` to commit messages unless the developer
|
|
115
|
-
explicitly requests it.
|
|
116
|
-
|
|
117
|
-
Rationale: the commit history is the developer's professional record. Co-authorship is
|
|
118
|
-
opt-in, not opt-out. If the developer wants to attribute AI involvement, they can add
|
|
119
|
-
it themselves or ask Claude to include it for a specific commit.
|
|
120
|
-
|
|
121
|
-
Before adding any co-authorship attribution, ask:
|
|
122
|
-
> "Would you like to add AI co-authorship to this commit, or keep it as your commit alone?"
|
|
123
|
-
|
|
124
|
-
Default answer if not asked: **developer only**.
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
### 5. Release the lock
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
rm .ai-assistance/bridge/LOCK
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Quick reference
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
# 0. Check lock
|
|
140
|
-
cat .ai-assistance/bridge/LOCK 2>/dev/null || echo "NO_LOCK"
|
|
141
|
-
|
|
142
|
-
# 1. Acquire lock
|
|
143
|
-
cat > .ai-assistance/bridge/LOCK << EOF
|
|
144
|
-
AGENT: code
|
|
145
|
-
USER: oscar
|
|
146
|
-
ACQUIRED: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
147
|
-
EXPIRES: $(date -u -d "+30 minutes" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+30M +"%Y-%m-%dT%H:%M:%SZ")
|
|
148
|
-
INTENT: <what you are changing>
|
|
149
|
-
FILES: <files>
|
|
150
|
-
EOF
|
|
151
|
-
|
|
152
|
-
# 2. Check for overlapping unstaged changes
|
|
153
|
-
git diff --name-only HEAD && git diff --cached --name-only
|
|
154
|
-
# If any of those files overlap with your planned FILES → commit them first:
|
|
155
|
-
git add -A && git commit -m "wip: <description of CoWork's in-progress work>"
|
|
156
|
-
# If no overlap → skip, proceed directly
|
|
157
|
-
|
|
158
|
-
# 3. Apply changes
|
|
159
|
-
# ... make edits ...
|
|
160
|
-
|
|
161
|
-
# 4. Commit your changes
|
|
162
|
-
git add -A && git commit -m "<descriptive message>"
|
|
163
|
-
|
|
164
|
-
# 5. Release lock
|
|
165
|
-
rm .ai-assistance/bridge/LOCK
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## Notes
|
|
171
|
-
|
|
172
|
-
- If Code crashes mid-protocol, the LOCK will expire naturally (30 min timeout)
|
|
173
|
-
- The `wip:` commit prefix signals to teammates that this was an auto-committed
|
|
174
|
-
in-progress state — safe to squash or amend later
|
|
175
|
-
- This protocol does not apply to bridge task processing (reading inbox, writing outbox)
|
|
176
|
-
— those are read/write of bridge files only and don't touch the working tree
|