jumbotron 0.1.7 → 0.3.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/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/public/register_sandbox_projection.rb +42 -0
- data/app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb +51 -0
- data/app/deserializers/jumbotron/deserializers/public/sandbox_projection.rb +17 -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/game.rb +11 -0
- data/app/models/jumbotron/public/translate_outcome.rb +13 -7
- data/app/models/jumbotron/public.rb +19 -1
- 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/canonical/upsert_game_graph.rb +126 -0
- data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +23 -3
- data/app/services/jumbotron/services/public/assemble_sandbox_projection.rb +39 -0
- data/app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb +58 -0
- data/app/services/jumbotron/services/public/read_sandbox_projection.rb +32 -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/read_projection_workflow.rb +28 -0
- 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/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/docs/upgrades/0.3.0.md +31 -0
- data/lib/jumbotron/client.rb +21 -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 +63 -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,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).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Upgrade: Jumbotron 0.3.0
|
|
2
|
+
|
|
3
|
+
**From:** `0.2.0`
|
|
4
|
+
**To:** `0.3.0`
|
|
5
|
+
|
|
6
|
+
Minor release: additive public read access for named sandbox projections and stable source labeling for host administration.
|
|
7
|
+
|
|
8
|
+
## Host-facing change
|
|
9
|
+
|
|
10
|
+
| Change | Host impact |
|
|
11
|
+
|--------|-------------|
|
|
12
|
+
| `Jumbotron.client.sandbox_projection` | Reads a named sandbox projection through the public workflow/client boundary without exposing persistence. Unknown names return the existing structured public failure contract. |
|
|
13
|
+
| `SandboxProjection#source_label` | Public projection DTOs, including register/Reset results, now include a host-ready source label such as `NFL 2026 Regular Season`. |
|
|
14
|
+
| Public read validation | Named projection reads use the same deserializer/service/workflow architecture as existing public operations. |
|
|
15
|
+
|
|
16
|
+
## Upgrade checklist
|
|
17
|
+
|
|
18
|
+
1. Bump the gem to `0.3.0` (or `>= 0.3.0`).
|
|
19
|
+
2. No new migration. Hosts already on `0.2.0` do not need to copy or run Jumbotron migrations for this release.
|
|
20
|
+
3. Hosts may read the configured source before offering a destructive Reset:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
projection = Jumbotron.client.sandbox_projection(name: "nfl-sandbox")
|
|
24
|
+
projection.source_label
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What is not in this release
|
|
28
|
+
|
|
29
|
+
- No sandbox projection or Reset persistence change.
|
|
30
|
+
- No ESPN public Game/Schedule/Consensus/CurrentLines contract change.
|
|
31
|
+
- No Pick'em administration UI or competitive-domain behavior.
|
data/lib/jumbotron/client.rb
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Jumbotron
|
|
4
4
|
class Client
|
|
5
|
+
def sandbox_projection(**)
|
|
6
|
+
Public::TranslateOutcome.call(
|
|
7
|
+
Workflows::Sandbox::ReadProjectionWorkflow.call(**),
|
|
8
|
+
payload_key: :sandbox_projection
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register_sandbox_projection(**)
|
|
13
|
+
Public::TranslateOutcome.call(
|
|
14
|
+
Workflows::Sandbox::RegisterProjectionWorkflow.call(**),
|
|
15
|
+
payload_key: :sandbox_projection
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset_sandbox_projection(**)
|
|
20
|
+
Public::TranslateOutcome.call(
|
|
21
|
+
Workflows::Sandbox::ResetProjectionWorkflow.call(**),
|
|
22
|
+
payload_key: :sandbox_reset
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
5
26
|
def schedule(**)
|
|
6
27
|
Public::TranslateOutcome.call(
|
|
7
28
|
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.3.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
|
|
@@ -105,14 +131,19 @@ files:
|
|
|
105
131
|
- app/deserializers/jumbotron/deserializers/public/game.rb
|
|
106
132
|
- app/deserializers/jumbotron/deserializers/public/game_ids.rb
|
|
107
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
|
|
136
|
+
- app/deserializers/jumbotron/deserializers/public/sandbox_projection.rb
|
|
108
137
|
- app/deserializers/jumbotron/deserializers/public/schedule.rb
|
|
109
138
|
- app/deserializers/jumbotron/deserializers/public/schedule_groups.rb
|
|
110
139
|
- app/errors/jumbotron/error.rb
|
|
111
140
|
- app/errors/jumbotron/errors/adapters.rb
|
|
112
141
|
- app/errors/jumbotron/errors/canonical.rb
|
|
113
142
|
- app/errors/jumbotron/errors/public.rb
|
|
143
|
+
- app/errors/jumbotron/errors/sandbox.rb
|
|
114
144
|
- app/errors/jumbotron/invalid_request_error.rb
|
|
115
145
|
- app/errors/jumbotron/not_found_error.rb
|
|
146
|
+
- app/errors/jumbotron/sandbox_reset_rejected_error.rb
|
|
116
147
|
- app/errors/jumbotron/unsupported_capability_error.rb
|
|
117
148
|
- app/jobs/jumbotron/application_job.rb
|
|
118
149
|
- app/jobs/jumbotron/discovery_job.rb
|
|
@@ -130,6 +161,10 @@ files:
|
|
|
130
161
|
- app/models/jumbotron/provider_identity.rb
|
|
131
162
|
- app/models/jumbotron/public.rb
|
|
132
163
|
- app/models/jumbotron/public/translate_outcome.rb
|
|
164
|
+
- app/models/jumbotron/sandbox_game_mapping.rb
|
|
165
|
+
- app/models/jumbotron/sandbox_game_plan.rb
|
|
166
|
+
- app/models/jumbotron/sandbox_projection.rb
|
|
167
|
+
- app/models/jumbotron/sandbox_projection_data.rb
|
|
133
168
|
- app/models/jumbotron/schedule_group.rb
|
|
134
169
|
- app/models/jumbotron/season.rb
|
|
135
170
|
- app/models/jumbotron/season_phase.rb
|
|
@@ -142,6 +177,9 @@ files:
|
|
|
142
177
|
- app/providers/jumbotron/providers/espn/resources/competition_odds.rb
|
|
143
178
|
- app/providers/jumbotron/providers/espn/resources/scoreboard.rb
|
|
144
179
|
- app/providers/jumbotron/providers/espn/resources/teams.rb
|
|
180
|
+
- app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb
|
|
181
|
+
- app/providers/jumbotron/providers/sandbox/resources/projection.rb
|
|
182
|
+
- app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb
|
|
145
183
|
- app/scheduling/jumbotron/scheduling/backends/sidekiq_cron.rb
|
|
146
184
|
- app/scheduling/jumbotron/scheduling/backends/solid_queue.rb
|
|
147
185
|
- app/scheduling/jumbotron/scheduling/clock.rb
|
|
@@ -176,20 +214,33 @@ files:
|
|
|
176
214
|
- app/services/jumbotron/services/canonical/resolve_current_lines.rb
|
|
177
215
|
- app/services/jumbotron/services/canonical/resolve_current_lines_for_games.rb
|
|
178
216
|
- app/services/jumbotron/services/canonical/upsert_game.rb
|
|
217
|
+
- app/services/jumbotron/services/canonical/upsert_game_graph.rb
|
|
179
218
|
- app/services/jumbotron/services/canonical/upsert_game_participant.rb
|
|
180
219
|
- app/services/jumbotron/services/canonical/upsert_team.rb
|
|
181
220
|
- app/services/jumbotron/services/canonical/upsert_venue.rb
|
|
182
221
|
- app/services/jumbotron/services/public/assemble_public_game.rb
|
|
183
222
|
- app/services/jumbotron/services/public/assemble_public_schedule.rb
|
|
223
|
+
- app/services/jumbotron/services/public/assemble_sandbox_projection.rb
|
|
224
|
+
- app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb
|
|
184
225
|
- app/services/jumbotron/services/public/derive_consensus_for_games.rb
|
|
185
226
|
- app/services/jumbotron/services/public/load_games_by_ids.rb
|
|
186
227
|
- app/services/jumbotron/services/public/load_included_lines.rb
|
|
187
228
|
- app/services/jumbotron/services/public/map_public_consensus_lines.rb
|
|
188
229
|
- app/services/jumbotron/services/public/map_public_current_lines.rb
|
|
189
230
|
- app/services/jumbotron/services/public/read_game.rb
|
|
231
|
+
- app/services/jumbotron/services/public/read_sandbox_projection.rb
|
|
190
232
|
- app/services/jumbotron/services/public/read_schedule.rb
|
|
191
233
|
- app/services/jumbotron/services/public/read_schedule_groups.rb
|
|
192
234
|
- app/services/jumbotron/services/public/resolve_current_lines_for_games.rb
|
|
235
|
+
- app/services/jumbotron/services/sandbox/build_source_snapshot.rb
|
|
236
|
+
- app/services/jumbotron/services/sandbox/project_game.rb
|
|
237
|
+
- app/services/jumbotron/services/sandbox/register_projection.rb
|
|
238
|
+
- app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb
|
|
239
|
+
- app/services/jumbotron/services/sandbox/remove_mapped_game.rb
|
|
240
|
+
- app/services/jumbotron/services/sandbox/resolve_game_observation.rb
|
|
241
|
+
- app/services/jumbotron/services/sandbox/seed_projected_game.rb
|
|
242
|
+
- app/services/jumbotron/services/sandbox/select_line_basis.rb
|
|
243
|
+
- app/services/jumbotron/services/sandbox/validate_projected_season.rb
|
|
193
244
|
- app/synchronization/jumbotron/synchronization/lease.rb
|
|
194
245
|
- app/workflows/jumbotron/workflows/execute_discovery_workflow.rb
|
|
195
246
|
- app/workflows/jumbotron/workflows/execute_game_update_policy_workflow.rb
|
|
@@ -200,6 +251,9 @@ files:
|
|
|
200
251
|
- app/workflows/jumbotron/workflows/read_game_workflow.rb
|
|
201
252
|
- app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb
|
|
202
253
|
- app/workflows/jumbotron/workflows/read_schedule_workflow.rb
|
|
254
|
+
- app/workflows/jumbotron/workflows/sandbox/read_projection_workflow.rb
|
|
255
|
+
- app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb
|
|
256
|
+
- app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb
|
|
203
257
|
- app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb
|
|
204
258
|
- db/migrate/20260812000001_create_jumbotron_foundational_catalog.rb
|
|
205
259
|
- db/migrate/20260812000002_create_jumbotron_game_graph.rb
|
|
@@ -211,10 +265,14 @@ files:
|
|
|
211
265
|
- db/migrate/20260827150001_add_key_to_jumbotron_teams.rb
|
|
212
266
|
- db/migrate/20260828180001_add_nickname_to_jumbotron_teams.rb
|
|
213
267
|
- db/migrate/20260828220001_add_game_presentation_contract_fields.rb
|
|
268
|
+
- db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb
|
|
269
|
+
- db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb
|
|
214
270
|
- docs/testing.md
|
|
215
271
|
- docs/upgrades/0.1.2.md
|
|
216
272
|
- docs/upgrades/0.1.3.md
|
|
217
273
|
- docs/upgrades/0.1.4.md
|
|
274
|
+
- docs/upgrades/0.2.0.md
|
|
275
|
+
- docs/upgrades/0.3.0.md
|
|
218
276
|
- lib/jumbotron.rb
|
|
219
277
|
- lib/jumbotron/client.rb
|
|
220
278
|
- lib/jumbotron/engine.rb
|
|
@@ -229,6 +287,9 @@ files:
|
|
|
229
287
|
- spec/factories/jumbotron/line_observation.rb
|
|
230
288
|
- spec/factories/jumbotron/observation_batch.rb
|
|
231
289
|
- spec/factories/jumbotron/provider_identity.rb
|
|
290
|
+
- spec/factories/jumbotron/sandbox_game_mapping.rb
|
|
291
|
+
- spec/factories/jumbotron/sandbox_game_plan.rb
|
|
292
|
+
- spec/factories/jumbotron/sandbox_projection.rb
|
|
232
293
|
- spec/factories/jumbotron/schedule_group.rb
|
|
233
294
|
- spec/factories/jumbotron/season.rb
|
|
234
295
|
- spec/factories/jumbotron/season_phase.rb
|