jumbotron 0.1.6 → 0.2.0

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/app/adapters/jumbotron/adapters/espn/nfl.rb +15 -0
  3. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +14 -2
  4. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +2 -1
  5. data/app/adapters/jumbotron/adapters/sandbox/nfl.rb +135 -0
  6. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/acquisition_scope.rb +74 -0
  7. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/competition_odds.rb +82 -0
  8. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/current_observation_state.rb +53 -0
  9. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/line_acquisition_scope.rb +30 -0
  10. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/monotonic_observation.rb +113 -0
  11. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/observation_values.rb +53 -0
  12. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/outcome_candidates.rb +38 -0
  13. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/projection.rb +87 -0
  14. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/scoreboard.rb +115 -0
  15. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/synthetic_observation.rb +32 -0
  16. data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/timeline_observation.rb +119 -0
  17. data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +14 -1
  18. data/app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb +27 -0
  19. data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +10 -1
  20. data/app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb +27 -0
  21. data/app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb +42 -0
  22. data/app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb +51 -0
  23. data/app/errors/jumbotron/error.rb +7 -0
  24. data/app/errors/jumbotron/errors/sandbox.rb +67 -0
  25. data/app/errors/jumbotron/sandbox_reset_rejected_error.rb +6 -0
  26. data/app/models/jumbotron/canonical.rb +20 -5
  27. data/app/models/jumbotron/game.rb +11 -0
  28. data/app/models/jumbotron/public/translate_outcome.rb +13 -7
  29. data/app/models/jumbotron/public.rb +31 -6
  30. data/app/models/jumbotron/sandbox_game_mapping.rb +24 -0
  31. data/app/models/jumbotron/sandbox_game_plan.rb +23 -0
  32. data/app/models/jumbotron/sandbox_projection.rb +30 -0
  33. data/app/models/jumbotron/sandbox_projection_data.rb +32 -0
  34. data/app/models/jumbotron/season.rb +10 -0
  35. data/app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb +24 -0
  36. data/app/providers/jumbotron/providers/sandbox/resources/projection.rb +23 -0
  37. data/app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb +24 -0
  38. data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +2 -1
  39. data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +1 -0
  40. data/app/services/jumbotron/services/adapters/select_eligible_games.rb +1 -1
  41. data/app/services/jumbotron/services/canonical/upsert_game_graph.rb +126 -0
  42. data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +114 -6
  43. data/app/services/jumbotron/services/canonical/upsert_team.rb +24 -0
  44. data/app/services/jumbotron/services/canonical/upsert_venue.rb +33 -1
  45. data/app/services/jumbotron/services/public/assemble_public_game.rb +20 -2
  46. data/app/services/jumbotron/services/public/assemble_sandbox_projection.rb +30 -0
  47. data/app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb +49 -0
  48. data/app/services/jumbotron/services/sandbox/build_source_snapshot.rb +224 -0
  49. data/app/services/jumbotron/services/sandbox/project_game.rb +73 -0
  50. data/app/services/jumbotron/services/sandbox/register_projection.rb +76 -0
  51. data/app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb +46 -0
  52. data/app/services/jumbotron/services/sandbox/remove_mapped_game.rb +29 -0
  53. data/app/services/jumbotron/services/sandbox/resolve_game_observation.rb +106 -0
  54. data/app/services/jumbotron/services/sandbox/seed_projected_game.rb +95 -0
  55. data/app/services/jumbotron/services/sandbox/select_line_basis.rb +69 -0
  56. data/app/services/jumbotron/services/sandbox/validate_projected_season.rb +77 -0
  57. data/app/workflows/jumbotron/workflows/public_read.rb +2 -1
  58. data/app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb +33 -0
  59. data/app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb +158 -0
  60. data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +3 -51
  61. data/db/migrate/20260828220001_add_game_presentation_contract_fields.rb +19 -0
  62. data/db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb +16 -0
  63. data/db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb +39 -0
  64. data/docs/upgrades/0.2.0.md +74 -0
  65. data/lib/jumbotron/client.rb +14 -0
  66. data/lib/jumbotron/version.rb +1 -1
  67. data/spec/factories/jumbotron/sandbox_game_mapping.rb +24 -0
  68. data/spec/factories/jumbotron/sandbox_game_plan.rb +10 -0
  69. data/spec/factories/jumbotron/sandbox_projection.rb +18 -0
  70. metadata +62 -2
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Workflows
5
+ module Sandbox
6
+ class ResetProjectionWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
+ include Jumbotron::Workflows::PublicRead
8
+
9
+ retry_strategy :none
10
+
11
+ def call(**kwargs)
12
+ deserialized = Jumbotron::Deserializers::Public::ResetSandboxProjection.call(kwargs)
13
+ return map_deserializer_failure(deserialized) unless deserialized.success?
14
+
15
+ request = deserialized.input
16
+ projection = Jumbotron::SandboxProjection.find_by(name: request.sandbox)
17
+ return missing_projection if projection.nil?
18
+ return unsafe_projection if unsafe_projection?(projection)
19
+
20
+ built = Jumbotron::Services::Sandbox::BuildSourceSnapshot.call(
21
+ projection: projection,
22
+ start_week: request.start_week,
23
+ anchor_day: request.anchor_day,
24
+ now: request.now
25
+ )
26
+ return map_service_failure(built) unless built.success?
27
+
28
+ reset_in_transaction(projection, built.data[:snapshot], request.now)
29
+ end
30
+
31
+ private
32
+
33
+ def reset_in_transaction(projection, snapshot, reset_at)
34
+ transaction do
35
+ projection = Jumbotron::SandboxProjection.lock.find(projection.id)
36
+ counts = exact_retry?(projection, snapshot) ? retry_counts : reconcile!(projection, snapshot, reset_at)
37
+ validation = Jumbotron::Services::Sandbox::ValidateProjectedSeason.call(
38
+ projection: projection,
39
+ snapshot: snapshot,
40
+ now: reset_at
41
+ )
42
+ fail_transaction!(map_service_failure(validation)) unless validation.success?
43
+
44
+ counts[:start_week] = validation.data[:start_week_game_count]
45
+ counts[:naturally_locked] = validation.data[:naturally_locked_game_count]
46
+ persist_success!(projection, snapshot, reset_at)
47
+ render_result!(projection, snapshot, counts, reset_at)
48
+ end
49
+ end
50
+
51
+ def reconcile!(projection, snapshot, reset_at)
52
+ change_set = Jumbotron::Canonical::ChangeSet.new
53
+ added = 0
54
+
55
+ snapshot.games.each do |projected_game|
56
+ projected = Jumbotron::Services::Sandbox::ProjectGame.call(
57
+ projection: projection,
58
+ projected_game: projected_game,
59
+ observed_at: reset_at,
60
+ change_set: change_set
61
+ )
62
+ fail_transaction!(map_service_failure(projected)) unless projected.success?
63
+ added += 1 if projected.data[:created]
64
+ end
65
+
66
+ removed = remove_stale!(projection, snapshot)
67
+ geometry = Jumbotron::Services::Sandbox::RemoveEmptyProjectionGeometry.call(projection: projection)
68
+ fail_transaction!(map_service_failure(geometry)) unless geometry.success?
69
+ seed_all!(projection, snapshot, reset_at, change_set)
70
+ {
71
+ added: added,
72
+ updated: snapshot.games.count - added,
73
+ removed: removed,
74
+ start_week: 0,
75
+ naturally_locked: 0
76
+ }
77
+ end
78
+
79
+ def remove_stale!(projection, snapshot)
80
+ current_source_ids = snapshot.games.map(&:source_game_id)
81
+ stale = projection.sandbox_game_mappings.where.not(source_game_id: current_source_ids).to_a
82
+ stale.each do |mapping|
83
+ result = Jumbotron::Services::Sandbox::RemoveMappedGame.call(mapping: mapping)
84
+ fail_transaction!(map_service_failure(result)) unless result.success?
85
+ end
86
+ stale.count
87
+ end
88
+
89
+ def seed_all!(projection, snapshot, reset_at, change_set)
90
+ mappings = projection.sandbox_game_mappings.index_by(&:source_game_id)
91
+ snapshot.games.each do |projected_game|
92
+ result = Jumbotron::Services::Sandbox::SeedProjectedGame.call(
93
+ game: mappings.fetch(projected_game.source_game_id).sandbox_game,
94
+ observed_at: reset_at,
95
+ change_set: change_set
96
+ )
97
+ fail_transaction!(map_service_failure(result)) unless result.success?
98
+ end
99
+ end
100
+
101
+ def render_result!(projection, snapshot, counts, reset_at)
102
+ assembled = Jumbotron::Services::Public::AssembleSandboxProjectionReset.call(
103
+ projection: projection,
104
+ snapshot: snapshot,
105
+ counts: counts,
106
+ reset_at: reset_at
107
+ )
108
+ fail_transaction!(map_service_failure(assembled)) unless assembled.success?
109
+
110
+ success(payload: { sandbox_reset: assembled.data[:public_reset] }, http_status: :ok)
111
+ end
112
+
113
+ def persist_success!(projection, snapshot, reset_at)
114
+ projection.update!(
115
+ last_reset_fingerprint: snapshot.fingerprint,
116
+ last_reset_at: reset_at,
117
+ last_date_shift_days: snapshot.date_shift_days
118
+ )
119
+ end
120
+
121
+ def exact_retry?(projection, snapshot)
122
+ projection.last_reset_fingerprint == snapshot.fingerprint
123
+ end
124
+
125
+ def retry_counts
126
+ { added: 0, updated: 0, removed: 0, start_week: 0, naturally_locked: 0 }
127
+ end
128
+
129
+ def unsafe_projection?(projection)
130
+ projection.adapter_id != "sandbox_nfl" ||
131
+ projection.sandbox_season.league.name != "nfl-sandbox" ||
132
+ projection.source_season_id == projection.sandbox_season_id
133
+ end
134
+
135
+ def missing_projection
136
+ failure(
137
+ errors: [Jumbotron::Errors::Sandbox::ProjectionNotFoundError.new],
138
+ http_status: :not_found
139
+ )
140
+ end
141
+
142
+ def unsafe_projection
143
+ failure(
144
+ errors: [
145
+ Jumbotron::Errors::Sandbox::ProjectionRejectedError.new(
146
+ details: {
147
+ code: "sandbox_target_forbidden",
148
+ message: "reset target is not a registered sandbox season"
149
+ }
150
+ )
151
+ ],
152
+ http_status: :unprocessable_entity
153
+ )
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -94,63 +94,15 @@ module Jumbotron
94
94
  end
95
95
 
96
96
  def sync_one_game!(game_input, league, observed_at, change_set)
97
- season_result = Services::Canonical::EnsureSeason.call(
98
- league: league,
99
- year: game_input.season_year
100
- )
101
- fail_on_service_failure!(season_result)
102
-
103
- phase_result = Services::Canonical::EnsureSeasonPhase.call(
104
- season: season_result.data[:season],
105
- phase_key: game_input.season_phase_key
106
- )
107
- fail_on_service_failure!(phase_result)
108
-
109
- group_result = Services::Canonical::EnsureScheduleGroup.call(
110
- season: season_result.data[:season],
111
- season_phase: phase_result.data[:season_phase],
112
- schedule_group_input: game_input.schedule_group
113
- )
114
- fail_on_service_failure!(group_result)
115
-
116
- venue_result = Services::Canonical::UpsertVenue.call(
117
- venue_input: game_input.venue,
118
- observed_at: observed_at,
119
- change_set: change_set
120
- )
121
- fail_on_service_failure!(venue_result)
122
-
123
- game_result = Services::Canonical::UpsertGame.call(
97
+ result = Services::Canonical::UpsertGameGraph.call(
124
98
  game_input: game_input,
125
99
  league: league,
126
- season: season_result.data[:season],
127
- season_phase: phase_result.data[:season_phase],
128
- schedule_group: group_result.data[:schedule_group],
129
- venue: venue_result.data[:venue],
130
100
  observed_at: observed_at,
131
101
  change_set: change_set
132
102
  )
133
- fail_on_service_failure!(game_result)
134
-
135
- game = game_result.data[:game]
136
-
137
- attach = Services::Canonical::AttachProviderIdentities.call(
138
- target: game,
139
- refs: game_input.provider_identities
140
- )
141
- fail_on_service_failure!(attach)
142
-
143
- game_input.participants.each do |participant_input|
144
- gp_result = Services::Canonical::UpsertGameParticipant.call(
145
- game: game,
146
- participant_input: participant_input,
147
- observed_at: observed_at,
148
- change_set: change_set
149
- )
150
- fail_on_service_failure!(gp_result)
151
- end
103
+ fail_on_service_failure!(result)
152
104
 
153
- game
105
+ result.data[:game]
154
106
  end
155
107
 
156
108
  def fail_on_service_failure!(service_result)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddGamePresentationContractFields < ActiveRecord::Migration[8.0]
4
+ def change
5
+ change_table :jumbotron_game_participants, bulk: true do |t|
6
+ t.string :record_summary_entering
7
+ t.string :record_summary_current
8
+ t.string :record_summary_post_game
9
+ t.datetime :record_observed_at
10
+ end
11
+
12
+ change_table :jumbotron_venues, bulk: true do |t|
13
+ t.string :city
14
+ t.string :region
15
+ end
16
+
17
+ add_column :jumbotron_teams, :abbreviation, :string
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronSandboxGamePlans < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :jumbotron_sandbox_game_plans do |t|
6
+ t.references :game,
7
+ null: false,
8
+ foreign_key: { to_table: :jumbotron_games },
9
+ index: { unique: true }
10
+ t.integer :home_score, null: false
11
+ t.integer :away_score, null: false
12
+ t.datetime :selected_at, null: false
13
+ t.timestamps
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJumbotronSandboxProjections < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :jumbotron_sandbox_projections do |t|
6
+ t.string :name, null: false
7
+ t.references :source_season, null: false, foreign_key: { to_table: :jumbotron_seasons }
8
+ t.references :source_anchor_phase, null: false, foreign_key: { to_table: :jumbotron_season_phases }
9
+ t.references :sandbox_season,
10
+ null: false,
11
+ foreign_key: { to_table: :jumbotron_seasons },
12
+ index: { unique: true }
13
+ t.string :adapter_id, null: false
14
+ t.string :calendar_time_zone, null: false
15
+ t.string :last_reset_fingerprint
16
+ t.datetime :last_reset_at
17
+ t.integer :last_date_shift_days
18
+ t.timestamps
19
+ end
20
+ add_index :jumbotron_sandbox_projections, :name, unique: true
21
+
22
+ create_table :jumbotron_sandbox_game_mappings do |t|
23
+ t.references :sandbox_projection, null: false, foreign_key: { to_table: :jumbotron_sandbox_projections }
24
+ t.references :source_game, null: false, foreign_key: { to_table: :jumbotron_games }
25
+ t.references :sandbox_game,
26
+ null: false,
27
+ foreign_key: { to_table: :jumbotron_games },
28
+ index: { unique: true }
29
+ t.decimal :home_spread, precision: 6, scale: 2, null: false
30
+ t.decimal :total, precision: 6, scale: 2, null: false
31
+ t.string :line_fingerprint, null: false
32
+ t.timestamps
33
+ end
34
+ add_index :jumbotron_sandbox_game_mappings,
35
+ %i[sandbox_projection_id source_game_id],
36
+ unique: true,
37
+ name: "idx_jumbotron_sandbox_mapping_source"
38
+ end
39
+ end
@@ -0,0 +1,74 @@
1
+ # Upgrade: Jumbotron 0.2.0
2
+
3
+ **From:** `0.1.7`
4
+ **To:** `0.2.0`
5
+
6
+ Minor release: named NFL sandbox projection and Reset/reprojection behind the public client. ESPN `espn_nfl` read and update contracts are unchanged.
7
+
8
+ ## Host-facing change
9
+
10
+ | Change | Host impact |
11
+ |--------|-------------|
12
+ | `sandbox_nfl` adapter | In-process NFL sandbox adapter (`league: nfl-sandbox`) with the same Game/Line policy set as ESPN. Not ESPN I/O. |
13
+ | `Jumbotron.client.register_sandbox_projection` | Idempotent named bind of a sandbox to immutable source selectors. Rejects conflicting redefinition. |
14
+ | `Jumbotron.client.reset_sandbox_projection` | Whole-season Reset: Eastern calendar-day shift, stable source→sandbox game mappings, complete projection or no commit. |
15
+ | Sandbox public errors | Structured `code` + safe `details` on public exceptions (`SandboxResetRejectedError` and mapped sandbox failures). |
16
+ | Recurring schedules | Desired schedules now include `jumbotron:sandbox_nfl:*` policy keys (still no `full_season` discovery cron). |
17
+ | Temporary `json < 3` gemspec constraint | Bundler will refuse json 3.x for any host that depends on this gem. Not a Jumbotron API change. See below. |
18
+
19
+ ## Upgrade checklist
20
+
21
+ 1. Bump gem to `0.2.0` (or `>= 0.2.0`).
22
+ 2. Copy engine migrations and migrate:
23
+
24
+ ```bash
25
+ bin/rails jumbotron:install:migrations
26
+ bin/rails db:migrate
27
+ ```
28
+
29
+ New engine files:
30
+
31
+ - `20260908000001_create_jumbotron_sandbox_game_plans.rb`
32
+ - `20260908000002_create_jumbotron_sandbox_projections.rb` (also creates `jumbotron_sandbox_game_mappings`)
33
+
34
+ 3. Rematerialize Solid Queue recurring YAML so `jumbotron:sandbox_nfl:*` keys appear if the host should run sandbox polls:
35
+
36
+ ```bash
37
+ bin/rails jumbotron:schedules:materialize
38
+ ```
39
+
40
+ Host-owned keys (`pickem:*`, `clear_solid_queue_finished_jobs`) are preserved. ESPN `jumbotron:espn_nfl:*` keys are unchanged.
41
+
42
+ 4. Optional: register then Reset a named projection from a host (no HTTP in Jumbotron):
43
+
44
+ ```ruby
45
+ Jumbotron.client.register_sandbox_projection(name: "nfl-sandbox", source: "nfl")
46
+ Jumbotron.client.reset_sandbox_projection(
47
+ sandbox: "nfl-sandbox",
48
+ start_week: 1,
49
+ anchor_day: 1,
50
+ now: Time.current.iso8601
51
+ )
52
+ ```
53
+
54
+ ## Temporary `json < 3` constraint
55
+
56
+ **Why.** json 3.0 made `JSON.parse` keyword-only. Released Rails 8.1.3.1 still does `JSON.parse(json, options)` with a positional Hash (default `{}` even when callers pass no options). That raises `ArgumentError: wrong number of arguments (given 2, expected 1)` on every `ActiveSupport::JSON.decode`, including ActiveRecord `:json` attributes (`ObservationBatch`, `LineObservation`, `HistoricalChange`) and ActionDispatch JSON request bodies.
57
+
58
+ A Gemfile pin in this engine repo only protects dummy-app CI. Hosts (Pick'em, others) resolve `json` from their own lockfiles. Putting `json < 3` in the **gemspec** makes Bundler enforce the ceiling wherever Jumbotron is a dependency.
59
+
60
+ This is a workaround for a Rails/json incompatibility, not a Jumbotron product dependency. Rails main / `8-1-stable` already splat (`JSON.parse(json, **options)`); no Rails gem that includes that fix was published at 0.2.0 (`8.1.3.1` is still latest).
61
+
62
+ **Undo when all of the following are true:**
63
+
64
+ 1. A Rails gem is released whose `ActiveSupport::JSON.decode` calls `JSON.parse(json, **options)` (or equivalent: no positional options Hash). Confirm in that gem's `activesupport/lib/active_support/json/decoding.rb`.
65
+ 2. Every Jumbotron host is on that Rails version (or newer). Pick'em today is `~> 8.1.3`, `>= 8.1.3.1`.
66
+ 3. Then remove `spec.add_dependency "json", "< 3"` from `jumbotron.gemspec`, `bundle install` in Docker, drop this section, and drop the packaging example that asserts the ceiling.
67
+
68
+ Do not undo just because a host Gemfile pins json, or because Jumbotron CI happens to lock 2.x.
69
+
70
+ ## What is not in this release
71
+
72
+ - No ESPN public Game/Schedule/Consensus/CurrentLines contract change.
73
+ - No Pick'em competitive domain. Hosts own whether they materialize sandbox seasons and whether production runs `sandbox_nfl` polls.
74
+ - No new `full_season` discovery job for sandbox (sandbox catalog is projected, not ESPN-discovered).
@@ -2,6 +2,20 @@
2
2
 
3
3
  module Jumbotron
4
4
  class Client
5
+ def register_sandbox_projection(**)
6
+ Public::TranslateOutcome.call(
7
+ Workflows::Sandbox::RegisterProjectionWorkflow.call(**),
8
+ payload_key: :sandbox_projection
9
+ )
10
+ end
11
+
12
+ def reset_sandbox_projection(**)
13
+ Public::TranslateOutcome.call(
14
+ Workflows::Sandbox::ResetProjectionWorkflow.call(**),
15
+ payload_key: :sandbox_reset
16
+ )
17
+ end
18
+
5
19
  def schedule(**)
6
20
  Public::TranslateOutcome.call(
7
21
  Workflows::ReadScheduleWorkflow.call(**),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jumbotron
4
- VERSION = "0.1.6"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :jumbotron_sandbox_game_mapping, class: "Jumbotron::SandboxGameMapping" do
5
+ association :sandbox_projection, factory: :jumbotron_sandbox_projection
6
+ source_game do
7
+ association(
8
+ :jumbotron_game,
9
+ season: sandbox_projection.source_season,
10
+ league: sandbox_projection.source_season.league
11
+ )
12
+ end
13
+ sandbox_game do
14
+ association(
15
+ :jumbotron_game,
16
+ season: sandbox_projection.sandbox_season,
17
+ league: sandbox_projection.sandbox_season.league
18
+ )
19
+ end
20
+ home_spread { BigDecimal("-3.5") }
21
+ total { BigDecimal("44.5") }
22
+ line_fingerprint { "line-v1" }
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :jumbotron_sandbox_game_plan, class: "Jumbotron::SandboxGamePlan" do
5
+ association :game, factory: :jumbotron_game
6
+ home_score { 24 }
7
+ away_score { 20 }
8
+ selected_at { Time.current }
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :jumbotron_sandbox_projection, class: "Jumbotron::SandboxProjection" do
5
+ sequence(:name) { |number| "sandbox-#{number}" }
6
+ association :source_season, factory: :jumbotron_season
7
+ source_anchor_phase do
8
+ association :jumbotron_season_phase, season: source_season, name: "regular_season"
9
+ end
10
+ sandbox_season do
11
+ sport = association(:jumbotron_sport, name: "football")
12
+ league = association(:jumbotron_league, sport: sport, name: "nfl-sandbox")
13
+ association(:jumbotron_season, league: league, name: name)
14
+ end
15
+ adapter_id { "sandbox_nfl" }
16
+ calendar_time_zone { "America/New_York" }
17
+ end
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jumbotron
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - matt-taylor
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: command_tower
@@ -44,6 +44,20 @@ dependencies:
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '9.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: json
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '3'
47
61
  description: Jumbotron owns canonical sports truth independently of ESPN and Pick'em.
48
62
  email:
49
63
  - ''
@@ -71,6 +85,18 @@ files:
71
85
  - app/adapters/jumbotron/adapters/operation.rb
72
86
  - app/adapters/jumbotron/adapters/policy_definition.rb
73
87
  - app/adapters/jumbotron/adapters/registry.rb
88
+ - app/adapters/jumbotron/adapters/sandbox/nfl.rb
89
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/acquisition_scope.rb
90
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/competition_odds.rb
91
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/current_observation_state.rb
92
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/line_acquisition_scope.rb
93
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/monotonic_observation.rb
94
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/observation_values.rb
95
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/outcome_candidates.rb
96
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/projection.rb
97
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/scoreboard.rb
98
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/synthetic_observation.rb
99
+ - app/adapters/jumbotron/adapters/sandbox/nfl_helpers/timeline_observation.rb
74
100
  - app/boot/jumbotron/boot/enqueue_discoveries.rb
75
101
  - app/clients/jumbotron/clients.rb
76
102
  - app/clients/jumbotron/clients/espn.rb
@@ -91,6 +117,7 @@ files:
91
117
  - app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/side_quote.rb
92
118
  - app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/team_odds.rb
93
119
  - app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb
120
+ - app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb
94
121
  - app/deserializers/jumbotron/deserializers/clients/espn/event.rb
95
122
  - app/deserializers/jumbotron/deserializers/clients/espn/scoreboard/get.rb
96
123
  - app/deserializers/jumbotron/deserializers/clients/espn/season.rb
@@ -99,18 +126,23 @@ files:
99
126
  - app/deserializers/jumbotron/deserializers/clients/espn/team.rb
100
127
  - app/deserializers/jumbotron/deserializers/clients/espn/teams/get.rb
101
128
  - app/deserializers/jumbotron/deserializers/clients/espn/venue.rb
129
+ - app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb
102
130
  - app/deserializers/jumbotron/deserializers/clients/espn/week.rb
103
131
  - app/deserializers/jumbotron/deserializers/public/game.rb
104
132
  - app/deserializers/jumbotron/deserializers/public/game_ids.rb
105
133
  - app/deserializers/jumbotron/deserializers/public/includes.rb
134
+ - app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb
135
+ - app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb
106
136
  - app/deserializers/jumbotron/deserializers/public/schedule.rb
107
137
  - app/deserializers/jumbotron/deserializers/public/schedule_groups.rb
108
138
  - app/errors/jumbotron/error.rb
109
139
  - app/errors/jumbotron/errors/adapters.rb
110
140
  - app/errors/jumbotron/errors/canonical.rb
111
141
  - app/errors/jumbotron/errors/public.rb
142
+ - app/errors/jumbotron/errors/sandbox.rb
112
143
  - app/errors/jumbotron/invalid_request_error.rb
113
144
  - app/errors/jumbotron/not_found_error.rb
145
+ - app/errors/jumbotron/sandbox_reset_rejected_error.rb
114
146
  - app/errors/jumbotron/unsupported_capability_error.rb
115
147
  - app/jobs/jumbotron/application_job.rb
116
148
  - app/jobs/jumbotron/discovery_job.rb
@@ -128,6 +160,10 @@ files:
128
160
  - app/models/jumbotron/provider_identity.rb
129
161
  - app/models/jumbotron/public.rb
130
162
  - app/models/jumbotron/public/translate_outcome.rb
163
+ - app/models/jumbotron/sandbox_game_mapping.rb
164
+ - app/models/jumbotron/sandbox_game_plan.rb
165
+ - app/models/jumbotron/sandbox_projection.rb
166
+ - app/models/jumbotron/sandbox_projection_data.rb
131
167
  - app/models/jumbotron/schedule_group.rb
132
168
  - app/models/jumbotron/season.rb
133
169
  - app/models/jumbotron/season_phase.rb
@@ -140,6 +176,9 @@ files:
140
176
  - app/providers/jumbotron/providers/espn/resources/competition_odds.rb
141
177
  - app/providers/jumbotron/providers/espn/resources/scoreboard.rb
142
178
  - app/providers/jumbotron/providers/espn/resources/teams.rb
179
+ - app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb
180
+ - app/providers/jumbotron/providers/sandbox/resources/projection.rb
181
+ - app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb
143
182
  - app/scheduling/jumbotron/scheduling/backends/sidekiq_cron.rb
144
183
  - app/scheduling/jumbotron/scheduling/backends/solid_queue.rb
145
184
  - app/scheduling/jumbotron/scheduling/clock.rb
@@ -174,11 +213,14 @@ files:
174
213
  - app/services/jumbotron/services/canonical/resolve_current_lines.rb
175
214
  - app/services/jumbotron/services/canonical/resolve_current_lines_for_games.rb
176
215
  - app/services/jumbotron/services/canonical/upsert_game.rb
216
+ - app/services/jumbotron/services/canonical/upsert_game_graph.rb
177
217
  - app/services/jumbotron/services/canonical/upsert_game_participant.rb
178
218
  - app/services/jumbotron/services/canonical/upsert_team.rb
179
219
  - app/services/jumbotron/services/canonical/upsert_venue.rb
180
220
  - app/services/jumbotron/services/public/assemble_public_game.rb
181
221
  - app/services/jumbotron/services/public/assemble_public_schedule.rb
222
+ - app/services/jumbotron/services/public/assemble_sandbox_projection.rb
223
+ - app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb
182
224
  - app/services/jumbotron/services/public/derive_consensus_for_games.rb
183
225
  - app/services/jumbotron/services/public/load_games_by_ids.rb
184
226
  - app/services/jumbotron/services/public/load_included_lines.rb
@@ -188,6 +230,15 @@ files:
188
230
  - app/services/jumbotron/services/public/read_schedule.rb
189
231
  - app/services/jumbotron/services/public/read_schedule_groups.rb
190
232
  - app/services/jumbotron/services/public/resolve_current_lines_for_games.rb
233
+ - app/services/jumbotron/services/sandbox/build_source_snapshot.rb
234
+ - app/services/jumbotron/services/sandbox/project_game.rb
235
+ - app/services/jumbotron/services/sandbox/register_projection.rb
236
+ - app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb
237
+ - app/services/jumbotron/services/sandbox/remove_mapped_game.rb
238
+ - app/services/jumbotron/services/sandbox/resolve_game_observation.rb
239
+ - app/services/jumbotron/services/sandbox/seed_projected_game.rb
240
+ - app/services/jumbotron/services/sandbox/select_line_basis.rb
241
+ - app/services/jumbotron/services/sandbox/validate_projected_season.rb
191
242
  - app/synchronization/jumbotron/synchronization/lease.rb
192
243
  - app/workflows/jumbotron/workflows/execute_discovery_workflow.rb
193
244
  - app/workflows/jumbotron/workflows/execute_game_update_policy_workflow.rb
@@ -198,6 +249,8 @@ files:
198
249
  - app/workflows/jumbotron/workflows/read_game_workflow.rb
199
250
  - app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb
200
251
  - app/workflows/jumbotron/workflows/read_schedule_workflow.rb
252
+ - app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb
253
+ - app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb
201
254
  - app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb
202
255
  - db/migrate/20260812000001_create_jumbotron_foundational_catalog.rb
203
256
  - db/migrate/20260812000002_create_jumbotron_game_graph.rb
@@ -208,10 +261,14 @@ files:
208
261
  - db/migrate/20260820000001_add_progress_to_jumbotron_games.rb
209
262
  - db/migrate/20260827150001_add_key_to_jumbotron_teams.rb
210
263
  - db/migrate/20260828180001_add_nickname_to_jumbotron_teams.rb
264
+ - db/migrate/20260828220001_add_game_presentation_contract_fields.rb
265
+ - db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb
266
+ - db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb
211
267
  - docs/testing.md
212
268
  - docs/upgrades/0.1.2.md
213
269
  - docs/upgrades/0.1.3.md
214
270
  - docs/upgrades/0.1.4.md
271
+ - docs/upgrades/0.2.0.md
215
272
  - lib/jumbotron.rb
216
273
  - lib/jumbotron/client.rb
217
274
  - lib/jumbotron/engine.rb
@@ -226,6 +283,9 @@ files:
226
283
  - spec/factories/jumbotron/line_observation.rb
227
284
  - spec/factories/jumbotron/observation_batch.rb
228
285
  - spec/factories/jumbotron/provider_identity.rb
286
+ - spec/factories/jumbotron/sandbox_game_mapping.rb
287
+ - spec/factories/jumbotron/sandbox_game_plan.rb
288
+ - spec/factories/jumbotron/sandbox_projection.rb
229
289
  - spec/factories/jumbotron/schedule_group.rb
230
290
  - spec/factories/jumbotron/season.rb
231
291
  - spec/factories/jumbotron/season_phase.rb