diogenes 0.1.5 → 0.1.6
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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/lib/diogenes/evaluation/decision_record.rb +117 -0
- data/lib/diogenes/evaluation/session.rb +40 -2
- data/lib/diogenes/templates/init/artifacts/decision_record.md.erb +29 -38
- data/lib/diogenes/version.rb +1 -1
- data/sig/generated/diogenes/evaluation/decision_record.rbs +72 -0
- data/sig/generated/diogenes/evaluation/session.rbs +6 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7437b70a7659349d8486c0f1e5c6cf7a6735b120e1710cb94d3dacf05ba85b8
|
|
4
|
+
data.tar.gz: ffeee3838b29625af1f2f07f55459fd96d1678cfa2e161160433d818c36826fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c533c33b568af4710da44f2ca825710bfed3c6d7ccfa4956303183817b36aca7facb46b469e3081998ce0ca4b21a01f61e12dcaaace3f711db5d2feb4721faa
|
|
7
|
+
data.tar.gz: 10ab65d16346cd75fb17f1c6e9a1fbbae742ce12a62255eb5e2845a2d65840c58510f15a5da9df6c439c29f02a66b5aae00d7ae768180c7d800d0f5b271235d5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.6](https://github.com/meaganewaller/diogenes/compare/diogenes/v0.1.5...diogenes/v0.1.6) (2026-06-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **evaluate:** generate decision record artifact after gate evaluation :rocket: ([#11](https://github.com/meaganewaller/diogenes/issues/11)) ([14d9ac2](https://github.com/meaganewaller/diogenes/commit/14d9ac2a12d4c61378c442a833d878bdf62ebc0b))
|
|
9
|
+
|
|
3
10
|
## [0.1.5](https://github.com/meaganewaller/diogenes/compare/diogenes/v0.1.4...diogenes/v0.1.5) (2026-06-27)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "erb"
|
|
5
|
+
require "date"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module Diogenes
|
|
9
|
+
module Evaluation
|
|
10
|
+
class DecisionRecord
|
|
11
|
+
DEFAULT_OUTPUT_DIR = "docs/decisions" #: String
|
|
12
|
+
BUNDLED_TEMPLATE = File.expand_path("../templates/init/artifacts/decision_record.md.erb", __dir__).freeze #: String
|
|
13
|
+
PROJECT_TEMPLATE = ".diogenes/artifacts/decision_record.md.erb" #: String
|
|
14
|
+
|
|
15
|
+
#: (description: String, results: Array[untyped], evaluator: String, ?output_dir: String) -> void
|
|
16
|
+
def initialize(description:, results:, evaluator:, output_dir: DEFAULT_OUTPUT_DIR)
|
|
17
|
+
@description = description
|
|
18
|
+
@results = results
|
|
19
|
+
@evaluator = evaluator
|
|
20
|
+
@output_dir = output_dir
|
|
21
|
+
@alternative = "" #: String
|
|
22
|
+
@conditions = "" #: String
|
|
23
|
+
@notes = "" #: String
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_writer :alternative #: String
|
|
27
|
+
attr_writer :conditions #: String
|
|
28
|
+
attr_writer :notes #: String
|
|
29
|
+
|
|
30
|
+
#: () -> String
|
|
31
|
+
def verdict
|
|
32
|
+
@verdict ||= @results.all?(&:passed?) ? "PROCEED" : "REJECT"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: (String) -> void
|
|
36
|
+
attr_writer :verdict
|
|
37
|
+
|
|
38
|
+
#: () -> String
|
|
39
|
+
def filename
|
|
40
|
+
"#{slugify(@description)}_decision.md"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
#: (cwd: String) -> String
|
|
44
|
+
def write(cwd:)
|
|
45
|
+
dir = File.join(cwd, @output_dir)
|
|
46
|
+
FileUtils.mkdir_p(dir)
|
|
47
|
+
path = File.join(dir, filename)
|
|
48
|
+
File.write(path, render(cwd:))
|
|
49
|
+
path
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
#: (cwd: String) -> String
|
|
55
|
+
def render(cwd:)
|
|
56
|
+
ctx = RenderContext.new(
|
|
57
|
+
feature_name: @description,
|
|
58
|
+
evaluator: @evaluator,
|
|
59
|
+
verdict: verdict,
|
|
60
|
+
results: @results,
|
|
61
|
+
alternative: @alternative,
|
|
62
|
+
conditions: @conditions,
|
|
63
|
+
notes: @notes
|
|
64
|
+
)
|
|
65
|
+
ERB.new(read_template(cwd:), trim_mode: "-").result(ctx.get_binding)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#: (cwd: String) -> String
|
|
69
|
+
def read_template(cwd:)
|
|
70
|
+
project = File.join(cwd, PROJECT_TEMPLATE)
|
|
71
|
+
File.read(File.exist?(project) ? project : BUNDLED_TEMPLATE)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#: (String) -> String
|
|
75
|
+
def slugify(text)
|
|
76
|
+
text.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class RenderContext
|
|
80
|
+
#: (feature_name: String, evaluator: String, verdict: String, results: Array[untyped], alternative: String, conditions: String, notes: String) -> void
|
|
81
|
+
def initialize(feature_name:, evaluator:, verdict:, results:, alternative:, conditions:, notes:)
|
|
82
|
+
@feature_name = feature_name
|
|
83
|
+
@evaluator = evaluator
|
|
84
|
+
@verdict = verdict
|
|
85
|
+
@results = results
|
|
86
|
+
@alternative = alternative
|
|
87
|
+
@conditions = conditions
|
|
88
|
+
@notes = notes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
attr_reader :feature_name
|
|
92
|
+
def feature_slug = @feature_name.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
93
|
+
attr_reader :evaluator
|
|
94
|
+
attr_reader :verdict
|
|
95
|
+
attr_reader :alternative
|
|
96
|
+
attr_reader :conditions
|
|
97
|
+
attr_reader :notes
|
|
98
|
+
|
|
99
|
+
#: (Symbol) -> String
|
|
100
|
+
def gate_result(key)
|
|
101
|
+
r = @results.find { |res| res.gate.key == key }
|
|
102
|
+
return "not evaluated" unless r
|
|
103
|
+
r.passed? ? "PASS" : "FAIL"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
#: (Symbol) -> String
|
|
107
|
+
def gate_reason(key)
|
|
108
|
+
r = @results.find { |res| res.gate.key == key }
|
|
109
|
+
return "not evaluated" unless r
|
|
110
|
+
r.passed? ? "Confirmed" : "Not confirmed"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def get_binding = binding
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -17,6 +17,7 @@ module Diogenes
|
|
|
17
17
|
@out = out
|
|
18
18
|
@err = err
|
|
19
19
|
@in = opts.fetch(:in, $stdin)
|
|
20
|
+
@cwd = opts.fetch(:cwd, Dir.pwd) #: String
|
|
20
21
|
@results = [] #: Array[untyped]
|
|
21
22
|
end
|
|
22
23
|
|
|
@@ -82,11 +83,48 @@ module Diogenes
|
|
|
82
83
|
|
|
83
84
|
#: () -> void
|
|
84
85
|
def prompt_decision_record
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
return unless ask_yes_no("Generate a decision record?")
|
|
87
|
+
|
|
88
|
+
record = DecisionRecord.new(
|
|
89
|
+
description: @description,
|
|
90
|
+
results: @results,
|
|
91
|
+
evaluator: detect_evaluator
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
collect_failure_details(record) if @results.any?(&:failed?)
|
|
95
|
+
|
|
96
|
+
path = record.write(cwd: @cwd)
|
|
97
|
+
@out.puts " Decision record written to #{path}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#: (DecisionRecord) -> void
|
|
101
|
+
def collect_failure_details(record)
|
|
102
|
+
@out.puts
|
|
103
|
+
@out.puts " One or more gates failed. How should this be recorded?"
|
|
104
|
+
@out.puts " [1] REJECT — this feature should not use AI"
|
|
105
|
+
@out.puts " [2] PROCEED WITH CONDITIONS — proceeding with documented mitigations"
|
|
106
|
+
@out.print " Your choice (default: 1): "
|
|
107
|
+
choice = @in.gets&.strip
|
|
108
|
+
|
|
109
|
+
if choice == "2"
|
|
110
|
+
record.verdict = "PROCEED WITH CONDITIONS"
|
|
111
|
+
@out.print " Describe the conditions or mitigations: "
|
|
112
|
+
record.conditions = @in.gets&.strip || ""
|
|
113
|
+
else
|
|
114
|
+
record.verdict = "REJECT"
|
|
115
|
+
@out.print " Describe the recommended software alternative: "
|
|
116
|
+
record.alternative = @in.gets&.strip || ""
|
|
87
117
|
end
|
|
88
118
|
end
|
|
89
119
|
|
|
120
|
+
#: () -> String
|
|
121
|
+
def detect_evaluator
|
|
122
|
+
name = `git config user.name 2>/dev/null`.strip
|
|
123
|
+
name.empty? ? ENV.fetch("USER", "Unknown") : name
|
|
124
|
+
rescue Errno::ENOENT
|
|
125
|
+
ENV.fetch("USER", "Unknown")
|
|
126
|
+
end
|
|
127
|
+
|
|
90
128
|
#: (String) -> bool
|
|
91
129
|
def ask_yes_no(question)
|
|
92
130
|
@out.print " #{question} "
|
|
@@ -1,53 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AI Feature Decision Record
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
**Feature:** <%= feature_name %>
|
|
4
|
+
**Date:** <%= Date.today.strftime("%Y-%m-%d") %>
|
|
5
|
+
**Evaluator:** <%= evaluator %>
|
|
6
|
+
**Verdict:** <%= verdict %>
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
# AI Feature Decision Record
|
|
8
|
+
---
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
**Date:** <%= Date.today.strftime("%Y-%m-%d") %>
|
|
12
|
-
**Evaluator:** <%= evaluator %>
|
|
13
|
-
**Verdict:** <%= verdict %>
|
|
10
|
+
## Gate Results
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
| Gate | Principle | Result | Reason |
|
|
13
|
+
|------|-----------|--------|--------|
|
|
14
|
+
| Failure Mode | Least surprise at scale | <%= gate_result(:failure_mode) %> | <%= gate_reason(:failure_mode) %> |
|
|
15
|
+
| User Verifiable | Trust requires verification | <%= gate_result(:user_verifiable) %> | <%= gate_reason(:user_verifiable) %> |
|
|
16
|
+
| Human in the Loop | Human-centered design, genuinely | <%= gate_result(:human_in_loop) %> | <%= gate_reason(:human_in_loop) %> |
|
|
17
|
+
| Observability | Craftsmanship — you wouldn't ship blind | <%= gate_result(:observability) %> | <%= gate_reason(:observability) %> |
|
|
18
|
+
| Right Tool | Convention over configuration | <%= gate_result(:right_tool) %> | <%= gate_reason(:right_tool) %> |
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
---
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|------|-----------|--------|--------|
|
|
21
|
-
| 1. Failure Mode | Least surprise at scale | <%= gate_result(:failure_mode) %> | <%= gate_reason(:failure_mode) %> |
|
|
22
|
-
| 2. User Verifiable | Trust requires verification | <%= gate_result(:user_verifiable) %> | <%= gate_reason(:user_verifiable) %> |
|
|
23
|
-
| 3. Human in Loop | Human-centered, genuinely | <%= gate_result(:human_in_loop) %> | <%= gate_reason(:human_in_loop) %> |
|
|
24
|
-
| 4. Observability | Craftsmanship | <%= gate_result(:observability) %> | <%= gate_reason(:observability) %> |
|
|
25
|
-
| 5. Right Tool | Convention over configuration | <%= gate_result(:right_tool) %> | <%= gate_reason(:right_tool) %> |
|
|
22
|
+
## Verdict: <%= verdict %>
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
<% if verdict == "REJECT" || verdict == "PROCEED WITH CONDITIONS" -%>
|
|
25
|
+
### Recommended Alternative or Mitigation
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
<%= alternative.empty? ? "_No alternative or mitigation described._" : alternative %>
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
<% end -%>
|
|
30
|
+
<% if verdict == "PROCEED WITH CONDITIONS" -%>
|
|
31
|
+
### Conditions for Proceeding
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
<% end %>
|
|
33
|
+
<%= conditions.empty? ? "_No conditions specified._" : conditions %>
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
<% end -%>
|
|
36
|
+
---
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
<% end %>
|
|
38
|
+
## Notes
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
<%= notes.empty? ? "_No additional notes._" : notes %>
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
---
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
*Generated by [Diogenes](https://github.com/meaganewaller/diogenes)*
|
|
52
|
-
TEMPLATE
|
|
53
|
-
end
|
|
44
|
+
*Generated by [Diogenes](https://github.com/meaganewaller/diogenes)*
|
data/lib/diogenes/version.rb
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Generated from lib/diogenes/evaluation/decision_record.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Diogenes
|
|
4
|
+
module Evaluation
|
|
5
|
+
class DecisionRecord
|
|
6
|
+
DEFAULT_OUTPUT_DIR: String
|
|
7
|
+
|
|
8
|
+
BUNDLED_TEMPLATE: String
|
|
9
|
+
|
|
10
|
+
PROJECT_TEMPLATE: String
|
|
11
|
+
|
|
12
|
+
# : (description: String, results: Array[untyped], evaluator: String, ?output_dir: String) -> void
|
|
13
|
+
def initialize: (description: String, results: Array[untyped], evaluator: String, ?output_dir: String) -> void
|
|
14
|
+
|
|
15
|
+
attr_writer alternative: String
|
|
16
|
+
|
|
17
|
+
attr_writer conditions: String
|
|
18
|
+
|
|
19
|
+
attr_writer notes: String
|
|
20
|
+
|
|
21
|
+
# : () -> String
|
|
22
|
+
def verdict: () -> String
|
|
23
|
+
|
|
24
|
+
# : (String) -> void
|
|
25
|
+
attr_writer verdict: untyped
|
|
26
|
+
|
|
27
|
+
# : () -> String
|
|
28
|
+
def filename: () -> String
|
|
29
|
+
|
|
30
|
+
# : (cwd: String) -> String
|
|
31
|
+
def write: (cwd: String) -> String
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# : (cwd: String) -> String
|
|
36
|
+
def render: (cwd: String) -> String
|
|
37
|
+
|
|
38
|
+
# : (cwd: String) -> String
|
|
39
|
+
def read_template: (cwd: String) -> String
|
|
40
|
+
|
|
41
|
+
# : (String) -> String
|
|
42
|
+
def slugify: (String) -> String
|
|
43
|
+
|
|
44
|
+
class RenderContext
|
|
45
|
+
# : (feature_name: String, evaluator: String, verdict: String, results: Array[untyped], alternative: String, conditions: String, notes: String) -> void
|
|
46
|
+
def initialize: (feature_name: String, evaluator: String, verdict: String, results: Array[untyped], alternative: String, conditions: String, notes: String) -> void
|
|
47
|
+
|
|
48
|
+
attr_reader feature_name: untyped
|
|
49
|
+
|
|
50
|
+
def feature_slug: () -> untyped
|
|
51
|
+
|
|
52
|
+
attr_reader evaluator: untyped
|
|
53
|
+
|
|
54
|
+
attr_reader verdict: untyped
|
|
55
|
+
|
|
56
|
+
attr_reader alternative: untyped
|
|
57
|
+
|
|
58
|
+
attr_reader conditions: untyped
|
|
59
|
+
|
|
60
|
+
attr_reader notes: untyped
|
|
61
|
+
|
|
62
|
+
# : (Symbol) -> String
|
|
63
|
+
def gate_result: (Symbol) -> String
|
|
64
|
+
|
|
65
|
+
# : (Symbol) -> String
|
|
66
|
+
def gate_reason: (Symbol) -> String
|
|
67
|
+
|
|
68
|
+
def get_binding: () -> untyped
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -34,6 +34,12 @@ module Diogenes
|
|
|
34
34
|
# : () -> void
|
|
35
35
|
def prompt_decision_record: () -> void
|
|
36
36
|
|
|
37
|
+
# : (DecisionRecord) -> void
|
|
38
|
+
def collect_failure_details: (DecisionRecord) -> void
|
|
39
|
+
|
|
40
|
+
# : () -> String
|
|
41
|
+
def detect_evaluator: () -> String
|
|
42
|
+
|
|
37
43
|
# : (String) -> bool
|
|
38
44
|
def ask_yes_no: (String) -> bool
|
|
39
45
|
|
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
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Meagan Waller
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- lib/diogenes/dsl/hook.rb
|
|
62
62
|
- lib/diogenes/dsl/rule.rb
|
|
63
63
|
- lib/diogenes/dsl/skill.rb
|
|
64
|
+
- lib/diogenes/evaluation/decision_record.rb
|
|
64
65
|
- lib/diogenes/evaluation/gate.rb
|
|
65
66
|
- lib/diogenes/evaluation/gates.rb
|
|
66
67
|
- lib/diogenes/evaluation/session.rb
|
|
@@ -79,6 +80,7 @@ files:
|
|
|
79
80
|
- sig/generated/diogenes/dsl/hook.rbs
|
|
80
81
|
- sig/generated/diogenes/dsl/rule.rbs
|
|
81
82
|
- sig/generated/diogenes/dsl/skill.rbs
|
|
83
|
+
- sig/generated/diogenes/evaluation/decision_record.rbs
|
|
82
84
|
- sig/generated/diogenes/evaluation/gate.rbs
|
|
83
85
|
- sig/generated/diogenes/evaluation/gates.rbs
|
|
84
86
|
- sig/generated/diogenes/evaluation/session.rbs
|