|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require Rails.root.join('lib/ec_data_source') |
| 4 | +require Rails.root.join('lib/ec_plant_feed') |
| 5 | + |
| 6 | +# Runs the ECHOcommunity narrative feed through SourceSynchronizer. The |
| 7 | +# reasoning lives in lib/ec_plant_feed.rb; the short version is that this is the |
| 8 | +# ENGLISH half of reconciliation, where the genuine conflicts are. Other locales |
| 9 | +# are additive and go through plants:backfill_translations instead. |
| 10 | +# |
| 11 | +# bin/rails plants:sync_plants[tmp/feed.json] # dry run |
| 12 | +# APPLY=true bin/rails plants:sync_plants[tmp/feed.json] # write |
| 13 | +# |
| 14 | +# Payload: {"plants": {"<uuid>": {"uses": "...", ...}}} — flat, one locale, as |
| 15 | +# emitted by tools/export_ec_narrative.py --for feed. |
| 16 | +namespace :plants do |
| 17 | + desc 'Sync English plant narrative from ECHOcommunity (dry run unless APPLY=true)' |
| 18 | + task :sync_plants, [:path] => :environment do |_t, args| |
| 19 | + path = args[:path] or abort 'usage: bin/rails plants:sync_plants[path/to/feed.json]' |
| 20 | + abort "file not found: #{path}" unless File.exist?(path) |
| 21 | + |
| 22 | + data_source = EcDataSource.existing or abort 'run sync:bootstrap first' |
| 23 | + plants = JSON.parse(File.read(path))['plants'] or abort "no 'plants' key in #{path}" |
| 24 | + apply = ENV['APPLY'] == 'true' |
| 25 | + run_id = ENV['RUN_ID'].presence || SecureRandom.hex(8) |
| 26 | + |
| 27 | + unlinked = Plant.unscoped.where(id: plants.keys, data_source_id: nil).count |
| 28 | + if unlinked.positive? |
| 29 | + abort "#{unlinked} of these plants are not linked to a data source. " \ |
| 30 | + 'Run sync:link_plants first, or the synchronizer will create duplicates.' |
| 31 | + end |
| 32 | + |
| 33 | + puts "#{apply ? 'SYNCING' : 'DRY RUN'} #{plants.size} plants, locale en" |
| 34 | + puts " data source: #{data_source.name} (#{data_source.id})" |
| 35 | + puts " run id: #{run_id}" |
| 36 | + puts |
| 37 | + |
| 38 | + result = EcPlantFeed.new(data_source: data_source, run_id: run_id) |
| 39 | + .run(plants, apply: apply) |
| 40 | + report = result.report |
| 41 | + unless report |
| 42 | + puts " rows built: #{result.rows.size} (dry run, nothing sent)" |
| 43 | + next |
| 44 | + end |
| 45 | + |
| 46 | + puts " applied (upstream won): #{report.applied}" |
| 47 | + puts " synced (already identical): #{report.synced}" |
| 48 | + puts " locally modified (local kept): #{report.locally_modified}" |
| 49 | + puts " CONFLICTS raised for review: #{report.conflicts_created}" |
| 50 | + puts " conflicts refreshed: #{report.conflicts_updated}" |
| 51 | + puts " created (new upstream record): #{report.created}" |
| 52 | + puts " invalid: #{report.invalid}" |
| 53 | + puts " errored: #{report.errored}" |
| 54 | + report.invalid_details.first(10).each { |d| puts " invalid: #{d}" } |
| 55 | + report.error_details.first(10).each { |d| puts " error: #{d}" } |
| 56 | + puts |
| 57 | + puts " open conflicts now: #{SyncConflict.where(status: 'open').count}" |
| 58 | + abort 'sync finished with errors' if report.errored.positive? |
| 59 | + end |
| 60 | +end |
0 commit comments