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.
- checksums.yaml +4 -4
- data/app/adapters/jumbotron/adapters/espn/nfl.rb +15 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +14 -2
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +2 -1
- data/app/adapters/jumbotron/adapters/sandbox/nfl.rb +135 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/acquisition_scope.rb +74 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/competition_odds.rb +82 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/current_observation_state.rb +53 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/line_acquisition_scope.rb +30 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/monotonic_observation.rb +113 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/observation_values.rb +53 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/outcome_candidates.rb +38 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/projection.rb +87 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/scoreboard.rb +115 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/synthetic_observation.rb +32 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/timeline_observation.rb +119 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +14 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +10 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb +42 -0
- data/app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb +51 -0
- data/app/errors/jumbotron/error.rb +7 -0
- data/app/errors/jumbotron/errors/sandbox.rb +67 -0
- data/app/errors/jumbotron/sandbox_reset_rejected_error.rb +6 -0
- data/app/models/jumbotron/canonical.rb +20 -5
- data/app/models/jumbotron/game.rb +11 -0
- data/app/models/jumbotron/public/translate_outcome.rb +13 -7
- data/app/models/jumbotron/public.rb +31 -6
- data/app/models/jumbotron/sandbox_game_mapping.rb +24 -0
- data/app/models/jumbotron/sandbox_game_plan.rb +23 -0
- data/app/models/jumbotron/sandbox_projection.rb +30 -0
- data/app/models/jumbotron/sandbox_projection_data.rb +32 -0
- data/app/models/jumbotron/season.rb +10 -0
- data/app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb +24 -0
- data/app/providers/jumbotron/providers/sandbox/resources/projection.rb +23 -0
- data/app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb +24 -0
- data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +2 -1
- data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +1 -0
- data/app/services/jumbotron/services/adapters/select_eligible_games.rb +1 -1
- data/app/services/jumbotron/services/canonical/upsert_game_graph.rb +126 -0
- data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +114 -6
- data/app/services/jumbotron/services/canonical/upsert_team.rb +24 -0
- data/app/services/jumbotron/services/canonical/upsert_venue.rb +33 -1
- data/app/services/jumbotron/services/public/assemble_public_game.rb +20 -2
- data/app/services/jumbotron/services/public/assemble_sandbox_projection.rb +30 -0
- data/app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb +49 -0
- data/app/services/jumbotron/services/sandbox/build_source_snapshot.rb +224 -0
- data/app/services/jumbotron/services/sandbox/project_game.rb +73 -0
- data/app/services/jumbotron/services/sandbox/register_projection.rb +76 -0
- data/app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb +46 -0
- data/app/services/jumbotron/services/sandbox/remove_mapped_game.rb +29 -0
- data/app/services/jumbotron/services/sandbox/resolve_game_observation.rb +106 -0
- data/app/services/jumbotron/services/sandbox/seed_projected_game.rb +95 -0
- data/app/services/jumbotron/services/sandbox/select_line_basis.rb +69 -0
- data/app/services/jumbotron/services/sandbox/validate_projected_season.rb +77 -0
- data/app/workflows/jumbotron/workflows/public_read.rb +2 -1
- data/app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb +33 -0
- data/app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb +158 -0
- data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +3 -51
- data/db/migrate/20260828220001_add_game_presentation_contract_fields.rb +19 -0
- data/db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb +16 -0
- data/db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb +39 -0
- data/docs/upgrades/0.2.0.md +74 -0
- data/lib/jumbotron/client.rb +14 -0
- data/lib/jumbotron/version.rb +1 -1
- data/spec/factories/jumbotron/sandbox_game_mapping.rb +24 -0
- data/spec/factories/jumbotron/sandbox_game_plan.rb +10 -0
- data/spec/factories/jumbotron/sandbox_projection.rb +18 -0
- 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
|
-
|
|
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!(
|
|
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).
|
data/lib/jumbotron/client.rb
CHANGED
|
@@ -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(**),
|
data/lib/jumbotron/version.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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
|