dogfood 0.1.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.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ class Randomness
5
+ attr_reader :seed
6
+
7
+ def initialize(seed: nil)
8
+ @seed = seed || Random.new_seed
9
+ @random = Random.new(@seed)
10
+ end
11
+
12
+ def branch(weights)
13
+ total = weights.values.sum.to_f
14
+ threshold = @random.rand * total
15
+ cumulative = 0.0
16
+
17
+ weights.each do |key, weight|
18
+ cumulative += weight
19
+ return key if threshold < cumulative
20
+ end
21
+
22
+ weights.keys.last
23
+ end
24
+
25
+ def pick(collection)
26
+ collection.sample(random: @random)
27
+ end
28
+
29
+ def sample(collection, count)
30
+ collection.sample(count, random: @random)
31
+ end
32
+
33
+ def rand(range = nil)
34
+ range ? @random.rand(range) : @random.rand
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Dogfood
6
+ class Renderer
7
+ TEMPLATE_PATH = File.expand_path("templates/day_log.md.erb", __dir__)
8
+
9
+ def initialize(result)
10
+ @result = result
11
+ end
12
+
13
+ def render
14
+ ERB.new(template, trim_mode: "-").result(binding)
15
+ end
16
+
17
+ private
18
+
19
+ def template
20
+ File.read(TEMPLATE_PATH)
21
+ end
22
+
23
+ def render_story(story)
24
+ lines = []
25
+ lines << "#### #{story[:id]}"
26
+ lines << "**Status:** #{story[:state][:status]}"
27
+ lines << "**Substatus:** #{story[:state][:substatus] || '-'}"
28
+ story[:steps].each do |step|
29
+ lines << "#{step.number}. **#{step.actor}** — #{step.event}"
30
+ lines << " - **Action:** #{step.action}"
31
+ lines << " - **Notes:** #{step.notes}" unless step.notes.to_s.empty?
32
+ end
33
+ lines.join("\n") + "\n"
34
+ end
35
+
36
+ def escape_table_cell(text)
37
+ text.to_s.gsub("|", "\\|").gsub("\n", "<br>")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ class Scheduler
5
+ DEFAULT_DELAYS = {
6
+ parts_arrival: { same_day: 0.40, next_day: 0.40, two_to_three_days: 0.15, next_week: 0.05 },
7
+ customer_approval: { same_day: 0.60, next_day: 0.25, two_to_three_days: 0.10, next_week: 0.05 },
8
+ payment: { same_day: 0.70, next_week: 0.20, net_30: 0.10 },
9
+ core_return: { next_day: 0.70, two_to_three_days: 0.30 },
10
+ labor_completion: { same_day: 0.85, next_day: 0.15 }
11
+ }.freeze
12
+
13
+ def initialize(rng, delays: {})
14
+ @rng = rng
15
+ @delays = DEFAULT_DELAYS.merge(delays)
16
+ end
17
+
18
+ def schedule(event)
19
+ distribution = @delays.fetch(event) { raise ArgumentError, "Unknown event: #{event}" }
20
+ delay_days = resolve_delay(distribution)
21
+ { delay_days: delay_days, event: event }
22
+ end
23
+
24
+ private
25
+
26
+ def resolve_delay(distribution)
27
+ total = distribution.values.sum.to_f
28
+ threshold = @rng.rand * total
29
+ cumulative = 0.0
30
+ distribution.each do |bucket, weight|
31
+ cumulative += weight
32
+ return delay_for(bucket) if threshold < cumulative
33
+ end
34
+ delay_for(distribution.keys.last)
35
+ end
36
+
37
+ def delay_for(bucket)
38
+ case bucket
39
+ when :same_day then 0
40
+ when :next_day then 1
41
+ when :two_to_three_days then @rng.rand(2..3)
42
+ when :next_week then 7
43
+ when :net_30 then 30
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ class Step
5
+ attr_reader :number, :actor, :event, :action, :notes
6
+
7
+ def initialize(number:, actor:, event:, action:, notes: nil)
8
+ @number = number
9
+ @actor = actor
10
+ @event = event
11
+ @action = action
12
+ @notes = notes || ""
13
+ end
14
+
15
+ def to_row
16
+ [number, actor, event, action, notes]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ class StoryBase
5
+ attr_reader :rng, :steps, :id
6
+
7
+ def initialize(rng:, id:)
8
+ @rng = rng
9
+ @id = id
10
+ @steps = []
11
+ @state = { status: :estimate, substatus: nil }
12
+ @step_number = 0
13
+ @next_stage = nil
14
+ end
15
+
16
+ def title
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def template_key
21
+ self.class.name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
22
+ end
23
+
24
+ def state
25
+ @state.dup
26
+ end
27
+
28
+ def advance(day_index:)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ protected
33
+
34
+ def step(actor:, event:, action:, notes: nil)
35
+ @step_number += 1
36
+ @steps << Dogfood::Step.new(
37
+ number: @step_number,
38
+ actor: actor,
39
+ event: event,
40
+ action: action,
41
+ notes: notes || ""
42
+ )
43
+ end
44
+
45
+ def update_state(status:, substatus:)
46
+ @state[:status] = status
47
+ @state[:substatus] = substatus
48
+ end
49
+
50
+ def branch(weights)
51
+ @rng.branch(weights)
52
+ end
53
+
54
+ def pick(collection)
55
+ @rng.pick(collection)
56
+ end
57
+
58
+ def sample(collection, count)
59
+ @rng.sample(collection, count)
60
+ end
61
+
62
+ def rand(range = nil)
63
+ @rng.rand(range)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ class StoryBuilder
5
+ def initialize(pack:, rng:)
6
+ @pack = pack
7
+ @rng = rng
8
+ @counter = 0
9
+ end
10
+
11
+ def build(fleet_ratio: 0.0, include: nil)
12
+ @counter += 1
13
+ id = "WO-#{@counter.to_s.rjust(3, '0')}"
14
+ name = include || @pack.scenario_keys.sample(random: @rng)
15
+ klass = @pack.find_scenario(name)
16
+ klass.new(rng: @rng, id: id)
17
+ end
18
+
19
+ def available_keys
20
+ @pack.scenario_keys
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ # Shop Simulation
2
+
3
+ **Generated:** <%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>
4
+ **Seed:** <%= @result[:seed] %>
5
+ **Days:** <%= @result[:days].length %>
6
+
7
+ <% @result[:days].each do |day| -%>
8
+ ---
9
+
10
+ ## Day <%= day[:index] + 1 %> — <%= day[:date] %>
11
+
12
+ ### Shift briefing
13
+ - New work orders today: <%= day[:morning].length + day[:afternoon].length + day[:evening].length - day[:carry_overs].length %>
14
+ - Carry-overs: <%= day[:carry_overs].length %>
15
+
16
+ <% unless day[:carry_overs].empty? -%>
17
+ ### Carry-overs
18
+ <% day[:carry_overs].each do |wo| -%>
19
+ - **<%= wo[:id] %>**: status `<%= wo[:status] %>` / `<%= wo[:substatus] %>`
20
+ <% end -%>
21
+ <% end -%>
22
+
23
+ ### Morning
24
+ <% day[:morning].each do |story| -%>
25
+ <%= render_story(story) %>
26
+ <% end -%>
27
+
28
+ ### Afternoon
29
+ <% day[:afternoon].each do |story| -%>
30
+ <%= render_story(story) %>
31
+ <% end -%>
32
+
33
+ ### Evening
34
+ <% day[:evening].each do |story| -%>
35
+ <%= render_story(story) %>
36
+ <% end -%>
37
+
38
+ ### End-of-day board
39
+ | WO | Status | Substatus | Next action |
40
+ |----|--------|-----------|-------------|
41
+ <% day[:end_of_day_board].each do |wo| -%>
42
+ | <%= escape_table_cell(wo[:id]) %> | <%= escape_table_cell(wo[:status]) %> | <%= escape_table_cell(wo[:substatus]) %> | <%= escape_table_cell(wo[:substatus] || "Continue") %> |
43
+ <% end -%>
44
+ <% end -%>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ module Test
5
+ # CI smoke-test helper. See SPEC.md 6.
6
+ module Smoke
7
+ def self.load_pack(path)
8
+ Dogfood::Pack.current = nil
9
+ load path # the pack file sets Dogfood::Pack.current
10
+ Dogfood::Pack.current or raise "Pack file did not set Dogfood::Pack.current"
11
+ end
12
+
13
+ def self.run_scenario(pack, name, days:, seed:)
14
+ pack.find_scenario(name)
15
+ generator = Dogfood::DayGenerator.new(pack: pack, days: days, seed: seed)
16
+ generator.generate # raises on any exception
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dogfood
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dogfood.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dogfood/version"
4
+ require_relative "dogfood/randomness"
5
+ require_relative "dogfood/scheduler"
6
+ require_relative "dogfood/ledger"
7
+ require_relative "dogfood/step"
8
+ require_relative "dogfood/story_base"
9
+ require_relative "dogfood/story_builder"
10
+ require_relative "dogfood/pack"
11
+ require_relative "dogfood/day_generator"
12
+ require_relative "dogfood/renderer"
13
+ require_relative "dogfood/dsl/evaluator"
14
+ require_relative "dogfood/dsl/schema"
15
+ require_relative "dogfood/dsl/compiler"
16
+ require_relative "dogfood/dsl/explain_renderer"
17
+ require_relative "dogfood/test/smoke"
18
+ require_relative "dogfood/cli"
19
+
20
+ module Dogfood
21
+ NestingTooDeep = Class.new(StandardError)
22
+ end
23
+
24
+ if defined?(Rails::Engine)
25
+ require_relative "dogfood/railtie"
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Dogfood
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def create_pack_file
11
+ template "dogfood.rb.tt", "config/dogfood.rb"
12
+ end
13
+
14
+ def create_scenarios_dir
15
+ empty_directory "config/dogfood"
16
+ template "example_scenario.yml.tt", "config/dogfood/example.yml"
17
+ end
18
+
19
+ def create_smoke_test
20
+ template "yml_smoke_test.rb.tt", "test/dogfood/yml_smoke_test.rb"
21
+ end
22
+
23
+ def show_readme
24
+ say "\nDogfood installed. Edit config/dogfood.rb to register your"
25
+ say "step modules, then drop scenarios into config/dogfood/*.yml."
26
+ say "Run `bin/rails dogfood --list` to see them.\n"
27
+ end
28
+ end
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dogfood
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben D'Angelo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: railties
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ description: Generates realistic multi-day workflows for UX testing. Domain-agnostic;
55
+ content supplied via packs.
56
+ email:
57
+ - ben@bendangelo.me
58
+ executables:
59
+ - dogfood
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - bin/dogfood
64
+ - lib/dogfood.rb
65
+ - lib/dogfood/cli.rb
66
+ - lib/dogfood/day_generator.rb
67
+ - lib/dogfood/dsl/compiler.rb
68
+ - lib/dogfood/dsl/evaluator.rb
69
+ - lib/dogfood/dsl/explain_renderer.rb
70
+ - lib/dogfood/dsl/schema.rb
71
+ - lib/dogfood/ledger.rb
72
+ - lib/dogfood/pack.rb
73
+ - lib/dogfood/railtie.rb
74
+ - lib/dogfood/randomness.rb
75
+ - lib/dogfood/renderer.rb
76
+ - lib/dogfood/scheduler.rb
77
+ - lib/dogfood/step.rb
78
+ - lib/dogfood/story_base.rb
79
+ - lib/dogfood/story_builder.rb
80
+ - lib/dogfood/templates/day_log.md.erb
81
+ - lib/dogfood/test/smoke.rb
82
+ - lib/dogfood/version.rb
83
+ - lib/generators/dogfood/install/install_generator.rb
84
+ homepage: https://github.com/bendangelo/dogfood
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://github.com/bendangelo/dogfood
89
+ source_code_uri: https://github.com/bendangelo/dogfood
90
+ bug_tracker_uri: https://github.com/bendangelo/dogfood/issues
91
+ changelog_uri: https://github.com/bendangelo/dogfood/blob/master/CHANGELOG.md
92
+ rubygems_mfa_required: 'true'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '3.1'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 4.0.15
108
+ specification_version: 4
109
+ summary: Generic multi-day scenario simulation engine
110
+ test_files: []