diogenes 0.1.4 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed59a2db9702739e79ab10146a799260ff41fbbe85c0178d9fd5c0976bd4621f
4
- data.tar.gz: 2ca1bf8250b1753b059b48ee226eb8b20c122c2adce7ada559bb1047b5c5a03f
3
+ metadata.gz: 00c7fad965257b62e461fbd2ae43db54c10bb7cc6c93d454cb2563ae1c6eb6a8
4
+ data.tar.gz: 69e47cf048ce0a2d31036ad803ebefcb0cf66bb6d6417ca517d2dc6cc52343d5
5
5
  SHA512:
6
- metadata.gz: 84d21f0c4cf3817bafa44054a479f0c8d91e4a26b19d6835313511c1e5108e0e49f96316391d5b58f5c1f6a569e1432f0481e07b50d59b2fe6401c52d8871f4e
7
- data.tar.gz: 2e5a9956eb13f0e19bd86cb71a333822771d4edd302e93c6000981b59ea9c67622a36702b9aee8f4e42b3f848b882b3ed44bcca3500fe2d38137ac4a47322ce6
6
+ metadata.gz: f3a0ad8ca1f41be1e4edfe1fe38552d954ecdf1d5d2dc6e417c713be956673c2ae67b0781fdb5720b693c9aab999b8a31c9b0eac55ccec5e16a3eb392c0decec
7
+ data.tar.gz: 42b1551a1bb754c6252d9fa0d04cdc0894251ac76f38656c9c1ae003d5854d59e816f4de3823fca4f2056a97d6a7b8b41026e112be6fa1b3a8ffbb8ecdac2164
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.1.4"
2
+ ".": "0.1.5"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.5](https://github.com/meaganewaller/diogenes/compare/diogenes/v0.1.4...diogenes/v0.1.5) (2026-06-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * **evaluate:** interactive five-gate evaluation command :fire: ([#9](https://github.com/meaganewaller/diogenes/issues/9)) ([cc43a17](https://github.com/meaganewaller/diogenes/commit/cc43a17b8f94fcd094b4d0fb806d9a3e43567fbe))
9
+
3
10
  ## [0.1.4](https://github.com/meaganewaller/diogenes/compare/diogenes/v0.1.3...diogenes/v0.1.4) (2026-06-27)
4
11
 
5
12
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Diogenes
5
+ class Cli
6
+ class Evaluate
7
+ #: (argv: Array[String], out: IO, err: IO, **untyped) -> Integer
8
+ def self.run(argv:, out:, err:, **opts)
9
+ description = argv.join(" ").strip
10
+
11
+ if description.empty?
12
+ err.puts "Usage: diogenes evaluate \"<feature description>\""
13
+ return 1
14
+ end
15
+
16
+ Evaluation::Session.new(description:, out:, err:, **opts).run
17
+ end
18
+ end
19
+ end
20
+ end
@@ -9,8 +9,10 @@ module Diogenes
9
9
  DIOGENES_DIR = ".diogenes" #: String
10
10
  TEMPLATES_ROOT = File.expand_path("../templates/init", __dir__) #: String
11
11
 
12
- #: (cwd: String, out: IO, err: IO) -> Integer
13
- def self.run(cwd:, out:, err:)
12
+ #: (cwd: String, out: IO, err: IO, ?argv: Array[String], **untyped) -> Integer
13
+ def self.run(cwd:, out:, err:, argv: [], **_opts)
14
+ raise Diogenes::UserError, "`init` takes no arguments" unless argv.empty?
15
+
14
16
  new(cwd: cwd, out: out, err: err).run
15
17
  end
16
18
 
data/lib/diogenes/cli.rb CHANGED
@@ -2,23 +2,26 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  require_relative "cli/init"
5
+ require_relative "cli/evaluate"
5
6
 
6
7
  module Diogenes
7
8
  class Cli
8
9
  COMMANDS = {
9
- "init" => Init
10
+ "init" => Init,
11
+ "evaluate" => Evaluate
10
12
  }.freeze #: Hash[String, Class]
11
13
 
12
- #: (?Array[String] argv, ?out: IO, ?err: IO) -> Integer
13
- def self.run(argv = ARGV, out: $stdout, err: $stderr)
14
- new(argv: argv, out: out, err: err).run
14
+ #: (?Array[String] argv, ?out: IO, ?err: IO, **untyped) -> Integer
15
+ def self.run(argv = ARGV, out: $stdout, err: $stderr, **opts)
16
+ new(argv: argv, out: out, err: err, **opts).run
15
17
  end
16
18
 
17
- #: (argv: Array[String], out: IO, err: IO) -> void
18
- def initialize(argv:, out:, err:)
19
+ #: (argv: Array[String], out: IO, err: IO, **untyped) -> void
20
+ def initialize(argv:, out:, err:, **opts)
19
21
  @argv = argv.dup #: Array[String]
20
22
  @out = out #: IO
21
23
  @err = err #: IO
24
+ @in = opts.fetch(:in, $stdin) #: IO
22
25
  end
23
26
 
24
27
  #: () -> Integer
@@ -42,13 +45,10 @@ module Diogenes
42
45
 
43
46
  private
44
47
 
45
- # Run a command
46
- # --
47
48
  #: (Class, Array[String]) -> Integer
48
49
  def run_command(command_class, args)
49
- raise UserError, "Unexpected arguments: #{args.join(" ")}" if args.any?
50
-
51
- command_class.run(cwd: Dir.pwd, out: @out, err: @err)
50
+ opts = {in: @in}
51
+ command_class.run(argv: args, cwd: Dir.pwd, out: @out, err: @err, **opts)
52
52
  end
53
53
 
54
54
  #: () -> Integer
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Diogenes
5
+ module Evaluation
6
+ class Gate
7
+ attr_reader :key #: Symbol
8
+ attr_reader :name #: String
9
+ attr_reader :principle #: String
10
+ attr_reader :explanation #: String
11
+ attr_reader :question #: String
12
+ attr_reader :fail_message #: String
13
+
14
+ #: (key: Symbol, name: String, principle: String, explanation: String, question: String, fail_message: String) -> void
15
+ def initialize(key:, name:, principle:, explanation:, question:, fail_message:)
16
+ @key = key
17
+ @name = name
18
+ @principle = principle
19
+ @explanation = explanation
20
+ @question = question
21
+ @fail_message = fail_message
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Diogenes
5
+ module Evaluation
6
+ module Gates
7
+ ALL = [
8
+ Gate.new(
9
+ key: :failure_mode,
10
+ name: "Failure Mode",
11
+ principle: "Least surprise at scale",
12
+ explanation: "When AI gets things wrong — and it will — what happens? " \
13
+ "This gate tests whether failure is detectable and recoverable. " \
14
+ "Embarrassing failures are manageable. Silent, cumulative, or " \
15
+ "catastrophic ones are not.",
16
+ question: "When this feature produces wrong output, can the user " \
17
+ "detect and recover without your direct intervention? (Y/n)",
18
+ fail_message: "Failures that go undetected or unrecoverable create compounding " \
19
+ "harm over time. Add explicit uncertainty signals, limit the feature " \
20
+ "scope, or move to a human-reviewed workflow."
21
+ ),
22
+ Gate.new(
23
+ key: :user_verifiable,
24
+ name: "User Verifiable",
25
+ principle: "Trust requires verification",
26
+ explanation: "Can a typical user of this feature actually tell when the " \
27
+ "output is wrong? If they need domain expertise they don't have to " \
28
+ "evaluate AI output, you're shipping a confidence gap, not a feature.",
29
+ question: "Can a typical user judge whether the output is correct — " \
30
+ "without needing domain expertise they don't already have? (Y/n)",
31
+ fail_message: "Shipping outputs users cannot evaluate creates misplaced trust. " \
32
+ "Consider adding confidence scores, human review, or limiting the " \
33
+ "feature to domains where users have the relevant expertise."
34
+ ),
35
+ Gate.new(
36
+ key: :human_in_loop,
37
+ name: "Human in the Loop",
38
+ principle: "Human-centered design, genuinely",
39
+ explanation: "A loop requires a human with time, context, and authority to act. " \
40
+ "If the human is approving outputs they cannot evaluate, that is a " \
41
+ "rubber stamp — not a loop.",
42
+ question: "Is there a specific person with enough time, context, and " \
43
+ "authority to review and override outputs before they affect users? (Y/n)",
44
+ fail_message: "A human in the loop who lacks time, context, or authority is " \
45
+ "safety theater. Either invest in a real review capacity or design " \
46
+ "the feature so it doesn't require human sign-off."
47
+ ),
48
+ Gate.new(
49
+ key: :observability,
50
+ name: "Observability",
51
+ principle: "Craftsmanship — you wouldn't ship blind",
52
+ explanation: "Silent degradation is worse than no feature. If you won't know " \
53
+ "when this AI feature starts producing worse outputs in production, " \
54
+ "you'll find out through a user complaint — after the damage is done.",
55
+ question: "Do you have (or will you have before shipping) monitoring to " \
56
+ "detect when this feature's quality degrades in production? (Y/n)",
57
+ fail_message: "Shipping an AI feature without observability means betting your " \
58
+ "users will tell you when it breaks. Set up quality monitoring, " \
59
+ "sampling, or feedback loops before you ship."
60
+ ),
61
+ Gate.new(
62
+ key: :right_tool,
63
+ name: "Right Tool",
64
+ principle: "Convention over configuration",
65
+ explanation: "AI is powerful, but it is not always the right answer. " \
66
+ "Deterministic software is more reliable, cheaper to operate, and " \
67
+ "easier to audit. This gate asks you to make the case for AI explicitly.",
68
+ question: "Have you genuinely considered and ruled out a simpler, " \
69
+ "deterministic software solution for this specific need? (Y/n)",
70
+ fail_message: "The best AI feature is sometimes no AI feature. Revisit whether " \
71
+ "the problem can be solved with rules, search, filtering, or structured " \
72
+ "data before adding an LLM dependency."
73
+ )
74
+ ].freeze #: Array[Gate]
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Diogenes
5
+ module Evaluation
6
+ class Session
7
+ SEPARATOR = ("─" * 60).freeze #: String
8
+
9
+ Result = Struct.new(:gate, :passed, keyword_init: true) do
10
+ def passed? = passed
11
+ def failed? = !passed
12
+ end
13
+
14
+ #: (description: String, out: IO, err: IO, **untyped) -> void
15
+ def initialize(description:, out:, err:, **opts)
16
+ @description = description
17
+ @out = out
18
+ @err = err
19
+ @in = opts.fetch(:in, $stdin)
20
+ @results = [] #: Array[untyped]
21
+ end
22
+
23
+ #: () -> Integer
24
+ def run
25
+ print_header
26
+
27
+ Gates::ALL.each_with_index do |gate, index|
28
+ run_gate(gate, index + 1)
29
+
30
+ if @results.last.failed? && !prompt_continue?
31
+ print_partial_summary
32
+ return 1
33
+ end
34
+ end
35
+
36
+ print_final_summary
37
+ prompt_decision_record
38
+
39
+ @results.all?(&:passed?) ? 0 : 1
40
+ rescue Interrupt
41
+ @out.puts "\n\nEvaluation interrupted."
42
+ print_partial_summary
43
+ 1
44
+ end
45
+
46
+ private
47
+
48
+ #: () -> void
49
+ def print_header
50
+ @out.puts
51
+ @out.puts "Evaluating: \"#{@description}\""
52
+ @out.puts
53
+ end
54
+
55
+ #: (Gate, Integer) -> void
56
+ def run_gate(gate, number)
57
+ @out.puts SEPARATOR
58
+ @out.puts "Gate #{number} of #{Gates::ALL.size} — #{gate.name}"
59
+ @out.puts "Principle: #{gate.principle}"
60
+ @out.puts SEPARATOR
61
+ @out.puts
62
+ @out.puts wrap(gate.explanation, 70)
63
+ @out.puts
64
+
65
+ passed = ask_yes_no(gate.question)
66
+ @results << Result.new(gate:, passed:)
67
+
68
+ if passed
69
+ @out.puts " ✓ Gate #{number} — PASS"
70
+ else
71
+ @out.puts " ✗ Gate #{number} — FAIL"
72
+ @out.puts
73
+ @out.puts " #{gate.fail_message}"
74
+ end
75
+ @out.puts
76
+ end
77
+
78
+ #: () -> bool
79
+ def prompt_continue?
80
+ ask_yes_no("Continue evaluating remaining gates? (Y/n)")
81
+ end
82
+
83
+ #: () -> void
84
+ def prompt_decision_record
85
+ if ask_yes_no("Generate a decision record? (Y/n)")
86
+ @out.puts " Decision record generation is coming in the next release."
87
+ end
88
+ end
89
+
90
+ #: (String) -> bool
91
+ def ask_yes_no(question)
92
+ @out.print " #{question} "
93
+ response = @in.gets&.strip&.downcase || ""
94
+ response.empty? || response.start_with?("y")
95
+ end
96
+
97
+ #: () -> void
98
+ def print_final_summary
99
+ if @results.all?(&:passed?)
100
+ @out.puts "All #{@results.size} gates passed! ✓"
101
+ else
102
+ @out.puts "Evaluation complete."
103
+ end
104
+ @out.puts
105
+ print_summary_table
106
+ @out.puts
107
+ end
108
+
109
+ #: () -> void
110
+ def print_partial_summary
111
+ @out.puts
112
+ @out.puts "Gates evaluated:"
113
+ print_summary_table
114
+ end
115
+
116
+ #: () -> void
117
+ def print_summary_table
118
+ Gates::ALL.each_with_index do |gate, index|
119
+ result = @results[index]
120
+ status = if result.nil?
121
+ "(not evaluated)"
122
+ elsif result.passed?
123
+ "PASS"
124
+ else
125
+ "FAIL"
126
+ end
127
+ @out.puts " Gate #{index + 1} — #{gate.name.ljust(22)} #{status}"
128
+ end
129
+ end
130
+
131
+ #: (String, Integer) -> String
132
+ def wrap(text, width)
133
+ text.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip
134
+ end
135
+ end
136
+ end
137
+ end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Diogenes
5
- VERSION = "0.1.4" #: String
5
+ VERSION = "0.1.5" #: String
6
6
  end
@@ -0,0 +1,10 @@
1
+ # Generated from lib/diogenes/cli/evaluate.rb with RBS::Inline
2
+
3
+ module Diogenes
4
+ class Cli
5
+ class Evaluate
6
+ # : (argv: Array[String], out: IO, err: IO, **untyped) -> Integer
7
+ def self.run: (argv: Array[String], out: IO, err: IO, **untyped) -> Integer
8
+ end
9
+ end
10
+ end
@@ -7,8 +7,8 @@ module Diogenes
7
7
 
8
8
  TEMPLATES_ROOT: String
9
9
 
10
- # : (cwd: String, out: IO, err: IO) -> Integer
11
- def self.run: (cwd: String, out: IO, err: IO) -> Integer
10
+ # : (cwd: String, out: IO, err: IO, ?argv: Array[String], **untyped) -> Integer
11
+ def self.run: (cwd: String, out: IO, err: IO, ?argv: Array[String], **untyped) -> Integer
12
12
 
13
13
  # : (cwd: String, out: IO, err: IO) -> void
14
14
  def initialize: (cwd: String, out: IO, err: IO) -> void
@@ -4,19 +4,17 @@ module Diogenes
4
4
  class Cli
5
5
  COMMANDS: Hash[String, Class]
6
6
 
7
- # : (?Array[String] argv, ?out: IO, ?err: IO) -> Integer
8
- def self.run: (?Array[String] argv, ?out: IO, ?err: IO) -> Integer
7
+ # : (?Array[String] argv, ?out: IO, ?err: IO, **untyped) -> Integer
8
+ def self.run: (?Array[String] argv, ?out: IO, ?err: IO, **untyped) -> Integer
9
9
 
10
- # : (argv: Array[String], out: IO, err: IO) -> void
11
- def initialize: (argv: Array[String], out: IO, err: IO) -> void
10
+ # : (argv: Array[String], out: IO, err: IO, **untyped) -> void
11
+ def initialize: (argv: Array[String], out: IO, err: IO, **untyped) -> void
12
12
 
13
13
  # : () -> Integer
14
14
  def run: () -> Integer
15
15
 
16
16
  private
17
17
 
18
- # Run a command
19
- # --
20
18
  # : (Class, Array[String]) -> Integer
21
19
  def run_command: (Class, Array[String]) -> Integer
22
20
 
@@ -0,0 +1,22 @@
1
+ # Generated from lib/diogenes/evaluation/gate.rb with RBS::Inline
2
+
3
+ module Diogenes
4
+ module Evaluation
5
+ class Gate
6
+ attr_reader key: Symbol
7
+
8
+ attr_reader name: String
9
+
10
+ attr_reader principle: String
11
+
12
+ attr_reader explanation: String
13
+
14
+ attr_reader question: String
15
+
16
+ attr_reader fail_message: String
17
+
18
+ # : (key: Symbol, name: String, principle: String, explanation: String, question: String, fail_message: String) -> void
19
+ def initialize: (key: Symbol, name: String, principle: String, explanation: String, question: String, fail_message: String) -> void
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ # Generated from lib/diogenes/evaluation/gates.rb with RBS::Inline
2
+
3
+ module Diogenes
4
+ module Evaluation
5
+ module Gates
6
+ ALL: Array[Gate]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ # Generated from lib/diogenes/evaluation/session.rb with RBS::Inline
2
+
3
+ module Diogenes
4
+ module Evaluation
5
+ class Session
6
+ SEPARATOR: String
7
+
8
+ class Result < Struct[untyped]
9
+ attr_accessor gate(): untyped
10
+
11
+ attr_accessor passed(): untyped
12
+
13
+ def self.new: (?gate: untyped, ?passed: untyped) -> instance
14
+ | ({ ?gate: untyped, ?passed: untyped }) -> instance
15
+ end
16
+
17
+ # : (description: String, out: IO, err: IO, **untyped) -> void
18
+ def initialize: (description: String, out: IO, err: IO, **untyped) -> void
19
+
20
+ # : () -> Integer
21
+ def run: () -> Integer
22
+
23
+ private
24
+
25
+ # : () -> void
26
+ def print_header: () -> void
27
+
28
+ # : (Gate, Integer) -> void
29
+ def run_gate: (Gate, Integer) -> void
30
+
31
+ # : () -> bool
32
+ def prompt_continue?: () -> bool
33
+
34
+ # : () -> void
35
+ def prompt_decision_record: () -> void
36
+
37
+ # : (String) -> bool
38
+ def ask_yes_no: (String) -> bool
39
+
40
+ # : () -> void
41
+ def print_final_summary: () -> void
42
+
43
+ # : () -> void
44
+ def print_partial_summary: () -> void
45
+
46
+ # : () -> void
47
+ def print_summary_table: () -> void
48
+
49
+ # : (String, Integer) -> String
50
+ def wrap: (String, Integer) -> String
51
+ end
52
+ end
53
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diogenes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Meagan Waller
@@ -55,11 +55,15 @@ files:
55
55
  - hk.pkl
56
56
  - lib/diogenes.rb
57
57
  - lib/diogenes/cli.rb
58
+ - lib/diogenes/cli/evaluate.rb
58
59
  - lib/diogenes/cli/init.rb
59
60
  - lib/diogenes/dsl/artifact.rb
60
61
  - lib/diogenes/dsl/hook.rb
61
62
  - lib/diogenes/dsl/rule.rb
62
63
  - lib/diogenes/dsl/skill.rb
64
+ - lib/diogenes/evaluation/gate.rb
65
+ - lib/diogenes/evaluation/gates.rb
66
+ - lib/diogenes/evaluation/session.rb
63
67
  - lib/diogenes/templates/init/artifacts/decision_record.md.erb
64
68
  - lib/diogenes/templates/init/diogenes.rb
65
69
  - lib/diogenes/templates/init/hooks/README.md
@@ -69,11 +73,15 @@ files:
69
73
  - sig/diogenes.rbs
70
74
  - sig/generated/diogenes.rbs
71
75
  - sig/generated/diogenes/cli.rbs
76
+ - sig/generated/diogenes/cli/evaluate.rbs
72
77
  - sig/generated/diogenes/cli/init.rbs
73
78
  - sig/generated/diogenes/dsl/artifact.rbs
74
79
  - sig/generated/diogenes/dsl/hook.rbs
75
80
  - sig/generated/diogenes/dsl/rule.rbs
76
81
  - sig/generated/diogenes/dsl/skill.rbs
82
+ - sig/generated/diogenes/evaluation/gate.rbs
83
+ - sig/generated/diogenes/evaluation/gates.rbs
84
+ - sig/generated/diogenes/evaluation/session.rbs
77
85
  - sig/generated/diogenes/version.rbs
78
86
  homepage: https://github.com/meaganewaller/diogenes/tree/main/diogenes
79
87
  licenses: