hecks 3.1.0 → 3.1.2
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/exe/hecks +1 -1
- data/lib/hecks/chapters.rb +4 -3
- data/lib/hecks/cli/domain_writer.rb +39 -0
- data/lib/hecks/cli/interview_agent.rb +59 -0
- data/lib/hecks/cli/interview_run.rb +87 -0
- data/lib/hecks/cli/interview_session.rb +299 -0
- data/lib/hecks/corpus.rb +1 -0
- data/lib/hecks/doors/cli_runner.rb +1 -1
- data/lib/hecks/doors/usage_cache.rb +123 -0
- data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
- data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
- data/lib/hecks/hecks/adapters/terminal.rb +15 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.bluebook +36 -0
- data/lib/hecks/hecks/hecks.bluebook +18 -0
- data/lib/hecks/hecks/hecks.hecksagon +8 -2
- data/lib/hecks/hecks/hecks.world +3 -2
- data/lib/hecks/projections/deploy/box/settings.rb +87 -4
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +18 -5
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +17 -1
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
- data/lib/hecks/projections/deploy/box.rb +216 -20
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +20 -4
- data/lib/hecks/projections/deploy/scripts/settings.rb +6 -0
- data/lib/hecks/projector/cli_projector.rb +38 -14
- data/lib/hecks/runtime/loader.rb +31 -8
- data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
- data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
- data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +1 -0
- data/rust/codegen/src/commands.rs +4 -32
- data/rust/codegen/src/json_codec.rs +3 -0
- data/rust/codegen/src/mutations.rs +74 -5
- data/rust/codegen/src/naming.rs +4 -0
- data/rust/codegen/src/types.rs +45 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/web/registration_receipt/tests.rs +39 -0
- data/rust/host/src/web/registration_receipt.rs +122 -0
- data/rust/host/src/web/registrations.rs +14 -2
- data/rust/host/src/web.rs +1 -0
- metadata +17 -2
|
@@ -400,16 +400,15 @@ module Hecks
|
|
|
400
400
|
return command_help(program, spec[:short], spec, ask: options[:ask]) if spec
|
|
401
401
|
end
|
|
402
402
|
|
|
403
|
-
width = (commands.values + questions.values).map { |spec| label(spec).length }.max.to_i
|
|
404
403
|
out = ["#{bluebook.name} — #{bluebook.vision}", "",
|
|
405
404
|
" #{program} <command>! [name=value …] do something",
|
|
406
405
|
" #{program} query <query> [name=value …] read something", ""]
|
|
407
406
|
|
|
408
407
|
out << "commands:"
|
|
409
|
-
out.concat(listing(commands
|
|
408
|
+
out.concat(listing(commands) { |spec| spec[:summary] })
|
|
410
409
|
out << ""
|
|
411
410
|
out << "queries (nothing here changes anything):"
|
|
412
|
-
out.concat(listing(questions
|
|
411
|
+
out.concat(listing(questions) { |spec| first_sentence(spec[:summary]) })
|
|
413
412
|
out << ""
|
|
414
413
|
out << " #{program} <command> --help what one command wants, and every way it refuses"
|
|
415
414
|
out << " a command is called with its aggregate — #{example_qualified(commands)}"
|
|
@@ -417,34 +416,59 @@ module Hecks
|
|
|
417
416
|
end
|
|
418
417
|
|
|
419
418
|
# The command or question lines of the help. A domain with more than one aggregate is listed
|
|
420
|
-
# under a heading per aggregate, so related commands sit together; the
|
|
419
|
+
# under a heading per aggregate, so related commands sit together; the heading is the prefix
|
|
420
|
+
# every call to them carries, so the lines under it leave it out. The bookkeeping a run
|
|
421
421
|
# records about itself (`internal`: system-role commands and port operations) is set apart as
|
|
422
|
-
# names only, since a person never types them. A single-aggregate domain keeps the plain
|
|
423
|
-
|
|
422
|
+
# names only, since a person never types them. A single-aggregate domain keeps the plain
|
|
423
|
+
# list, each name in full.
|
|
424
|
+
def listing(specs)
|
|
424
425
|
shown, internal = specs.values.partition { |spec| !spec[:internal] }
|
|
425
426
|
groups = shown.group_by { |spec| spec[:group] }
|
|
427
|
+
grouped = groups.length > 1
|
|
428
|
+
named = shown.to_h { |spec| [spec, entry_name(spec, grouped)] }
|
|
429
|
+
width = named.values.map(&:length).max.to_i
|
|
426
430
|
lines = []
|
|
427
|
-
if
|
|
431
|
+
if grouped
|
|
428
432
|
groups.each do |group, members|
|
|
429
433
|
lines << " #{heading(group)}" if group
|
|
430
|
-
members.each { |spec| lines << " #{
|
|
434
|
+
members.each { |spec| lines << " #{named[spec].ljust(width)} #{yield(spec)}#{alias_note(spec)}" }
|
|
431
435
|
end
|
|
432
436
|
else
|
|
433
|
-
shown.each { |spec| lines << " #{
|
|
437
|
+
shown.each { |spec| lines << " #{named[spec].ljust(width)} #{yield(spec)}#{alias_note(spec)}" }
|
|
434
438
|
end
|
|
435
439
|
lines.concat(internal_lines(internal)) unless internal.empty?
|
|
436
440
|
lines
|
|
437
441
|
end
|
|
438
442
|
|
|
439
|
-
#
|
|
440
|
-
# these aggregates share, so it adds nothing under a heading ("Test suite", "Model check").
|
|
443
|
+
# The aggregate's own name as a heading: the prefix of every call to the lines under it.
|
|
441
444
|
def heading(group)
|
|
442
|
-
|
|
443
|
-
|
|
445
|
+
"#{Naming.snake(group)}:"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# A spec's name as listed: under its aggregate's heading the aggregate prefix is left out.
|
|
449
|
+
# A command the chapter gives a short name (`mcp`) is listed by its real name, so the
|
|
450
|
+
# heading and the line still spell a call; `alias_note` says the short name.
|
|
451
|
+
def entry_name(spec, grouped)
|
|
452
|
+
name = label(spec, real: true)
|
|
453
|
+
return name unless grouped && spec[:group]
|
|
454
|
+
|
|
455
|
+
name.delete_prefix("#{Naming.snake(spec[:group])}.")
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# " (also: mcp!)" for a spec the chapter gave a short name, else nothing. A short name that
|
|
459
|
+
# is only the command's own name (`init` for `door.init`) is already in the line.
|
|
460
|
+
def alias_note(spec)
|
|
461
|
+
return "" if spec[:short_was].nil? || spec[:short_was].split(".").last == spec[:short]
|
|
462
|
+
|
|
463
|
+
" (also: #{label(spec)})"
|
|
444
464
|
end
|
|
445
465
|
|
|
446
466
|
# A command is written with the `!` that marks it; a query without one.
|
|
447
|
-
|
|
467
|
+
# With `real:`, a short name the chapter gave it gives way to the name it was given for.
|
|
468
|
+
def label(spec, real: false)
|
|
469
|
+
name = real && spec.key?(:short_was) ? spec[:short_was] : spec[:short]
|
|
470
|
+
spec[:kind] == :command ? "#{name}!" : name
|
|
471
|
+
end
|
|
448
472
|
|
|
449
473
|
# The internal commands as bare names, wrapped, under one line saying what they are.
|
|
450
474
|
def internal_lines(specs)
|
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -50,7 +50,26 @@ module Hecks
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# What `describe` answers: the loaded declarations and nothing bound to run them.
|
|
53
|
-
|
|
53
|
+
#
|
|
54
|
+
# The declarations load the first time `registry` is asked for, not when `describe` returns,
|
|
55
|
+
# so a caller that can answer from `directory` alone (a launcher with its help already
|
|
56
|
+
# remembered) never pays for the load. The directory is checked at once.
|
|
57
|
+
class Described
|
|
58
|
+
# @return [String] the domain directory that was described
|
|
59
|
+
attr_reader :directory
|
|
60
|
+
|
|
61
|
+
# @param directory [String] the domain directory
|
|
62
|
+
# @yield loads the declarations; answers the registry
|
|
63
|
+
def initialize(directory, &load)
|
|
64
|
+
@directory = directory
|
|
65
|
+
@load = load
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Registry] the declarations, loaded on first use
|
|
69
|
+
def registry
|
|
70
|
+
@registry ||= @load.call
|
|
71
|
+
end
|
|
72
|
+
end
|
|
54
73
|
|
|
55
74
|
# Loads `path`'s declarations into a fresh Registry and stops: no boot gate runs, no
|
|
56
75
|
# persistence adapter is resolved or bound, nothing connects to a database.
|
|
@@ -66,15 +85,19 @@ module Hecks
|
|
|
66
85
|
def self.describe(path, shared: nil, environment: FROM_ENV)
|
|
67
86
|
loading = Ports::Loading.bootstrap
|
|
68
87
|
directory = loading.bluebook_directory(path)
|
|
69
|
-
|
|
70
|
-
registry = Registry.new(root: File.dirname(directory))
|
|
88
|
+
overlay = selected_environment(environment)
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
loading.
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
Described.new(directory) do
|
|
91
|
+
root = loading.shared_root(shared, directory)
|
|
92
|
+
registry = Registry.new(root: File.dirname(directory))
|
|
93
|
+
|
|
94
|
+
Hecks.with_registry(registry) do
|
|
95
|
+
loading.load_library
|
|
96
|
+
loading.load_project(root)
|
|
97
|
+
loading.load_domain(directory, environment: overlay)
|
|
98
|
+
end
|
|
99
|
+
registry
|
|
76
100
|
end
|
|
77
|
-
Described.new(registry, directory)
|
|
78
101
|
end
|
|
79
102
|
|
|
80
103
|
# The overlay a boot loads: the caller's own choice (nil meaning none), else the
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# The Tickets chapter: what a maintainer observed about Hecks that needs a decision or a fix, the
|
|
2
|
+
# and the decisions that settle them.
|
|
3
|
+
#
|
|
4
|
+
# QualityControl has its own Ticket, the issue raised for a Bug in an outside tracker, and the
|
|
5
|
+
# Hecks domain has its own Release, the record of a gem version. A Finding sits before both: it is
|
|
6
|
+
# any observation, not yet a proven bug, and it needs no sweep or failing test to be recorded.
|
|
7
|
+
#
|
|
8
|
+
# A Finding is the record. A gap, a parity divergence, a QA finding and an idea are kinds of it, not
|
|
9
|
+
# separate aggregates. Hecks owns the finding's state; GitHub is driven from it through the
|
|
10
|
+
# GitHubIssues port, which a hecksagon binds (nothing here reads GitHub back). A finding exists whether
|
|
11
|
+
# or not GitHub answers: each lifecycle event asks the port, and a refused ask is recorded on the
|
|
12
|
+
# finding as a sync failure that a later run retries.
|
|
13
|
+
#
|
|
14
|
+
# An Adr is a decision record with its own lifecycle, linked to the finding it settles.
|
|
15
|
+
Hecks.bluebook "Tickets" do
|
|
16
|
+
vision "Track what Hecks needs, decide it, ship it, and drive GitHub from the record."
|
|
17
|
+
|
|
18
|
+
aggregate "Finding" do
|
|
19
|
+
description "One thing observed about Hecks that needs a decision or a fix: reported, triaged, then resolved or dismissed."
|
|
20
|
+
|
|
21
|
+
attribute :finding, FindingKey
|
|
22
|
+
attribute :title, Title
|
|
23
|
+
attribute :body, Body, optional: true
|
|
24
|
+
attribute :kind, Kind, optional: true
|
|
25
|
+
attribute :severity, Severity, optional: true
|
|
26
|
+
attribute :source, Source
|
|
27
|
+
attribute :fingerprint, Fingerprint, optional: true
|
|
28
|
+
attribute :issue_number, Number, optional: true
|
|
29
|
+
attribute :fix_pr_number, Number, optional: true
|
|
30
|
+
attribute :dismissal, Body, optional: true
|
|
31
|
+
attribute :sync_failure, Body, optional: true
|
|
32
|
+
identified_by :finding
|
|
33
|
+
|
|
34
|
+
value_object "FindingKey" do
|
|
35
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
36
|
+
|
|
37
|
+
invariant("a finding is named") { !value.to_s.empty? }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
value_object "Title" do
|
|
41
|
+
attribute :value, String
|
|
42
|
+
|
|
43
|
+
invariant("a finding has a title") { !value.to_s.empty? }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
value_object "Body" do
|
|
47
|
+
attribute :value, String
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# What the finding is: a `gap` Hecks cannot yet express, a `parity` divergence between the Ruby
|
|
51
|
+
# and Rust runtimes, a `qa` finding from the QA loop, or an `idea`.
|
|
52
|
+
value_object "Kind" do
|
|
53
|
+
attribute :value, String, one_of: ["gap", "parity", "qa", "idea"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
value_object "Severity" do
|
|
57
|
+
attribute :value, String, one_of: ["low", "medium", "high"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Where it came from: the QA loop, work for a client, a parity check, or a maintainer by hand.
|
|
61
|
+
value_object "Source" do
|
|
62
|
+
attribute :value, String, one_of: ["qa_loop", "client_work", "parity_check", "maintainer"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The same finding refiled by the QA loop carries the same fingerprint, so a report of it while an
|
|
66
|
+
# open finding holds it changes nothing.
|
|
67
|
+
value_object "Fingerprint" do
|
|
68
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
value_object "Number" do
|
|
72
|
+
attribute :value, Integer
|
|
73
|
+
|
|
74
|
+
invariant("a number is positive") { value.positive? }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
lifecycle :status, default: "reported" do
|
|
78
|
+
transition "Triage" => "triaged", from: %w[reported triaged]
|
|
79
|
+
transition "Resolve" => "resolved", from: "triaged"
|
|
80
|
+
transition "Dismiss" => "dismissed", from: %w[reported triaged]
|
|
81
|
+
transition "Reopen" => "triaged", from: %w[resolved dismissed]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
command "Report" do
|
|
85
|
+
goal "Record something observed about Hecks as a finding"
|
|
86
|
+
|
|
87
|
+
attribute :finding, FindingKey
|
|
88
|
+
attribute :title, Title
|
|
89
|
+
attribute :body, Body, optional: true
|
|
90
|
+
attribute :source, Source
|
|
91
|
+
attribute :fingerprint, Fingerprint, optional: true
|
|
92
|
+
|
|
93
|
+
sets :finding
|
|
94
|
+
sets :title
|
|
95
|
+
sets :body
|
|
96
|
+
sets :source
|
|
97
|
+
sets :fingerprint
|
|
98
|
+
|
|
99
|
+
emits "FindingReported"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A failed Codebase run reports itself: the run's key names the finding and doubles as its
|
|
103
|
+
# fingerprint, so the same run failing again while its finding is open changes nothing.
|
|
104
|
+
command "ReportFailedRun" do
|
|
105
|
+
goal "Record a failed conformance or gate run as a finding"
|
|
106
|
+
|
|
107
|
+
attribute :finding, FindingKey
|
|
108
|
+
attribute :body, Body
|
|
109
|
+
|
|
110
|
+
sets :finding
|
|
111
|
+
sets :body
|
|
112
|
+
sets :fingerprint, to: "run"
|
|
113
|
+
sets :title, to: "A Codebase run failed"
|
|
114
|
+
sets :source, to: "qa_loop"
|
|
115
|
+
|
|
116
|
+
emits "RunFindingReported"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
command "Triage" do
|
|
120
|
+
role "Maintainer"
|
|
121
|
+
goal "Give a finding its kind and severity"
|
|
122
|
+
|
|
123
|
+
reference_to Finding
|
|
124
|
+
attribute :kind, Kind
|
|
125
|
+
attribute :severity, Severity
|
|
126
|
+
|
|
127
|
+
sets :kind
|
|
128
|
+
sets :severity
|
|
129
|
+
|
|
130
|
+
emits "FindingTriaged"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
command "LinkFix" do
|
|
134
|
+
role "Maintainer"
|
|
135
|
+
goal "Record the pull request that fixes a triaged finding"
|
|
136
|
+
|
|
137
|
+
reference_to Finding
|
|
138
|
+
attribute :fix_pr_number, Number
|
|
139
|
+
|
|
140
|
+
sets :fix_pr_number
|
|
141
|
+
|
|
142
|
+
emits "FixLinked"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
command "Resolve" do
|
|
146
|
+
role "Maintainer"
|
|
147
|
+
goal "Close a triaged finding as resolved"
|
|
148
|
+
|
|
149
|
+
reference_to Finding
|
|
150
|
+
|
|
151
|
+
emits "FindingResolved"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
command "Dismiss" do
|
|
155
|
+
role "Maintainer"
|
|
156
|
+
goal "Close a finding without a fix, with the reason"
|
|
157
|
+
|
|
158
|
+
reference_to Finding
|
|
159
|
+
attribute :dismissal, Body
|
|
160
|
+
|
|
161
|
+
sets :dismissal
|
|
162
|
+
|
|
163
|
+
emits "FindingDismissed"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
command "Reopen" do
|
|
167
|
+
role "Maintainer"
|
|
168
|
+
goal "Take a resolved or dismissed finding back to triaged, as when a fix regresses"
|
|
169
|
+
|
|
170
|
+
reference_to Finding
|
|
171
|
+
|
|
172
|
+
emits "FindingReopened"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
command "RecordIssue" do
|
|
176
|
+
goal "Record the GitHub issue number once the issue is opened"
|
|
177
|
+
|
|
178
|
+
reference_to Finding
|
|
179
|
+
attribute :issue_number, Number
|
|
180
|
+
|
|
181
|
+
sets :issue_number
|
|
182
|
+
|
|
183
|
+
emits "IssueRecorded"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
command "RecordSyncFailure" do
|
|
187
|
+
goal "Record that GitHub could not be driven, so a later run retries"
|
|
188
|
+
|
|
189
|
+
reference_to Finding
|
|
190
|
+
attribute :sync_failure, Body
|
|
191
|
+
|
|
192
|
+
sets :sync_failure
|
|
193
|
+
|
|
194
|
+
emits "SyncFailed"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
query "Open" do
|
|
198
|
+
description "Every finding not yet resolved or dismissed."
|
|
199
|
+
where(status: { in: "reported,triaged" })
|
|
200
|
+
order_by :finding
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
aggregate "Adr" do
|
|
205
|
+
description "A decision record: drafted, accepted, and possibly superseded, linked to the tickets it settles."
|
|
206
|
+
|
|
207
|
+
attribute :adr, AdrNumber
|
|
208
|
+
attribute :title, AdrTitle
|
|
209
|
+
attribute :supersedes, AdrNumber, optional: true
|
|
210
|
+
attribute :settles, FindingKey, optional: true
|
|
211
|
+
identified_by :adr
|
|
212
|
+
|
|
213
|
+
value_object "AdrNumber" do
|
|
214
|
+
attribute :value, String, pattern: '\A[0-9]{4}\z'
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
value_object "AdrTitle" do
|
|
218
|
+
attribute :value, String
|
|
219
|
+
|
|
220
|
+
invariant("a decision has a title") { !value.to_s.empty? }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
value_object "FindingKey" do
|
|
224
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
lifecycle :status, default: "draft" do
|
|
228
|
+
transition "Accept" => "accepted", from: "draft"
|
|
229
|
+
transition "Supersede" => "superseded", from: "accepted"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
command "Draft" do
|
|
233
|
+
role "Maintainer"
|
|
234
|
+
goal "Open a decision record, optionally for the finding it settles"
|
|
235
|
+
|
|
236
|
+
attribute :adr, AdrNumber
|
|
237
|
+
attribute :title, AdrTitle
|
|
238
|
+
attribute :settles, FindingKey, optional: true
|
|
239
|
+
|
|
240
|
+
sets :adr
|
|
241
|
+
sets :title
|
|
242
|
+
sets :settles
|
|
243
|
+
|
|
244
|
+
emits "AdrDrafted"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
command "Accept" do
|
|
248
|
+
role "Maintainer"
|
|
249
|
+
goal "Record that the decision was accepted"
|
|
250
|
+
|
|
251
|
+
reference_to Adr
|
|
252
|
+
|
|
253
|
+
emits "AdrAccepted"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
command "Supersede" do
|
|
257
|
+
role "Maintainer"
|
|
258
|
+
goal "Record that a later decision replaces this one"
|
|
259
|
+
|
|
260
|
+
reference_to Adr
|
|
261
|
+
attribute :supersedes, AdrNumber
|
|
262
|
+
|
|
263
|
+
sets :supersedes
|
|
264
|
+
|
|
265
|
+
emits "AdrSuperseded"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
query "Standing" do
|
|
269
|
+
description "Every decision that is accepted and not superseded."
|
|
270
|
+
where(status: "accepted")
|
|
271
|
+
order_by :adr
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Each lifecycle event drives GitHub. A finding's own state never waits on the answer.
|
|
276
|
+
policy "OpenIssueWhenReported" do
|
|
277
|
+
on "Finding.FindingReported"
|
|
278
|
+
trigger Finding::GitHubIssues::OpenIssue, with: { finding: :finding }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
policy "OpenIssueWhenRunReported" do
|
|
282
|
+
on "Finding.RunFindingReported"
|
|
283
|
+
trigger Finding::GitHubIssues::OpenIssue, with: { finding: :finding }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
policy "LabelIssueWhenTriaged" do
|
|
287
|
+
on "Finding.FindingTriaged"
|
|
288
|
+
trigger Finding::GitHubIssues::LabelIssue, with: { finding: :finding }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
policy "CommentWhenFixLinked" do
|
|
292
|
+
on "Finding.FixLinked"
|
|
293
|
+
trigger Finding::GitHubIssues::CommentOnIssue, with: { finding: :finding }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
policy "CloseIssueWhenResolved" do
|
|
297
|
+
on "Finding.FindingResolved"
|
|
298
|
+
trigger Finding::GitHubIssues::CloseIssue, with: { finding: :finding }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
policy "CloseIssueWhenDismissed" do
|
|
302
|
+
on "Finding.FindingDismissed"
|
|
303
|
+
trigger Finding::GitHubIssues::CloseIssue, with: { finding: :finding }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
policy "ReopenIssueWhenReopened" do
|
|
307
|
+
on "Finding.FindingReopened"
|
|
308
|
+
trigger Finding::GitHubIssues::ReopenIssue, with: { finding: :finding }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# A conformance or gate run that faults files its own finding.
|
|
312
|
+
policy "ReportWhenConformanceFaulted" do
|
|
313
|
+
on "ConformanceRun.ConformanceFaulted"
|
|
314
|
+
trigger Finding::ReportFailedRun, with: { finding: :run, body: :refusal }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
policy "ReportWhenGateFaulted" do
|
|
318
|
+
on "GateRun.GateFaulted"
|
|
319
|
+
trigger Finding::ReportFailedRun, with: { finding: :run, body: :refusal }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
policy "RecordTheIssue" do
|
|
323
|
+
on "Finding.IssueOpened"
|
|
324
|
+
trigger Finding::RecordIssue, with: { issue_number: :issue_number }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
policy "RecordTheSyncFailure" do
|
|
328
|
+
on "Finding.GitHubRefused"
|
|
329
|
+
trigger Finding::RecordSyncFailure, with: { sync_failure: :refusal }
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The Tickets domain, bound so it boots standing alone (a fuzz or model_check boot of this
|
|
2
|
+
# directory). The GitHubIssues port every attaching hecksagon shares is in tickets.ports.hecksagon.
|
|
3
|
+
Hecks.hecksagon "Tickets" do
|
|
4
|
+
attaches "Governance"
|
|
5
|
+
persisted_by "Memory"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# The anti-corruption layer for Governance, which the chapter's roles are checked against.
|
|
9
|
+
Hecks.hecksagon "Governance" do
|
|
10
|
+
persisted_by "Memory"
|
|
11
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# The port Tickets declares, for every hecksagon that attaches the chapter (`Chapters` loads this
|
|
2
|
+
# file with it). Each lifecycle event of a finding asks the GitHubIssues port, which the Hecks domain
|
|
3
|
+
# binds (lib/hecks/hecks/adapters/finding_github.rb): the outside a finding reaches. The ask is
|
|
4
|
+
# answered with the record's own arguments or refused with a reason, and a refusal is recorded on the
|
|
5
|
+
# finding and never blocks it. The port has to be declared even where no adapter binds it, so the
|
|
6
|
+
# policies have a target and model_check's deaf-policy check can see it.
|
|
7
|
+
#
|
|
8
|
+
# Only OpenIssue answers with the issue number, so only its answer is recorded on the finding; the
|
|
9
|
+
# other asks answer with a line saying what was done.
|
|
10
|
+
Hecks.hecksagon "Tickets" do
|
|
11
|
+
Tickets::Finding.port "GitHubIssues" do
|
|
12
|
+
asks "OpenIssue", to: Finding do
|
|
13
|
+
attribute :finding, FindingKey
|
|
14
|
+
answers "IssueOpened"
|
|
15
|
+
refuses "GitHubRefused"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
%w[LabelIssue CommentOnIssue CloseIssue ReopenIssue].each do |ask|
|
|
19
|
+
asks ask, to: Finding do
|
|
20
|
+
attribute :finding, FindingKey
|
|
21
|
+
answers "GitHubAnswered"
|
|
22
|
+
refuses "GitHubRefused"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/hecks/version.rb
CHANGED
data/lib/hecks.rb
CHANGED
|
@@ -29,6 +29,7 @@ require_relative "hecks/projector"
|
|
|
29
29
|
require_relative "hecks/projections"
|
|
30
30
|
# After `Projector`, which dispatches against the `:cli` projection.
|
|
31
31
|
require_relative "hecks/doors/cli_door"
|
|
32
|
+
require_relative "hecks/doors/usage_cache"
|
|
32
33
|
require_relative "hecks/doors/cli_runner"
|
|
33
34
|
require_relative "hecks/storehouse"
|
|
34
35
|
require_relative "hecks/mcp_stdio_guard"
|
|
@@ -444,14 +444,7 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
|
|
|
444
444
|
.collect();
|
|
445
445
|
|
|
446
446
|
let transition = mutations::lifecycle_transition_for(command, aggregate);
|
|
447
|
-
let transition_arg =
|
|
448
|
-
Some(t) => format!(
|
|
449
|
-
"Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
|
|
450
|
-
naming::ruby_inspect_string(&t.field),
|
|
451
|
-
t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
|
|
452
|
-
),
|
|
453
|
-
None => "None".to_string(),
|
|
454
|
-
};
|
|
447
|
+
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
455
448
|
|
|
456
449
|
let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
|
|
457
450
|
// A `delegate` mutation is rendered by `delegation_of`, which overwrites `mutation_lines`;
|
|
@@ -772,14 +765,7 @@ fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_ob
|
|
|
772
765
|
})
|
|
773
766
|
.collect();
|
|
774
767
|
let transition = mutations::lifecycle_transition_for(target, entity);
|
|
775
|
-
let transition_arg =
|
|
776
|
-
Some(t) => format!(
|
|
777
|
-
"Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
|
|
778
|
-
naming::ruby_inspect_string(&t.field),
|
|
779
|
-
t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
|
|
780
|
-
),
|
|
781
|
-
None => "None".to_string(),
|
|
782
|
-
};
|
|
768
|
+
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
783
769
|
let mut mutation_lines: Vec<String> = target
|
|
784
770
|
.get("mutations")
|
|
785
771
|
.map(Json::each)
|
|
@@ -896,14 +882,7 @@ pub fn emit_entity_command(
|
|
|
896
882
|
.collect();
|
|
897
883
|
|
|
898
884
|
let transition = mutations::lifecycle_transition_for(command, entity);
|
|
899
|
-
let transition_arg =
|
|
900
|
-
Some(t) => format!(
|
|
901
|
-
"Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
|
|
902
|
-
naming::ruby_inspect_string(&t.field),
|
|
903
|
-
t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
|
|
904
|
-
),
|
|
905
|
-
None => "None".to_string(),
|
|
906
|
-
};
|
|
885
|
+
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
907
886
|
|
|
908
887
|
let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
|
|
909
888
|
let mut mutation_lines: Vec<String> = mutations_list.iter().map(|m| mutations::emit_mutation_line(exemplar, m, entity, command, value_objects_by_name, false)).collect();
|
|
@@ -1063,14 +1042,7 @@ pub fn emit_nested_entity_command(
|
|
|
1063
1042
|
|
|
1064
1043
|
// The nested entity's lifecycle, as in `emit_entity_command`.
|
|
1065
1044
|
let transition = mutations::lifecycle_transition_for(command, nested);
|
|
1066
|
-
let transition_arg =
|
|
1067
|
-
Some(t) => format!(
|
|
1068
|
-
"Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
|
|
1069
|
-
naming::ruby_inspect_string(&t.field),
|
|
1070
|
-
t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
|
|
1071
|
-
),
|
|
1072
|
-
None => "None".to_string(),
|
|
1073
|
-
};
|
|
1045
|
+
let transition_arg = mutations::transition_check_arg(transition.as_ref());
|
|
1074
1046
|
|
|
1075
1047
|
let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
|
|
1076
1048
|
let mut mutation_lines: Vec<String> = mutations_list.iter().map(|m| mutations::emit_mutation_line(exemplar, m, nested, command, value_objects_by_name, false)).collect();
|
|
@@ -44,6 +44,7 @@ pub fn scalar_json_accessor(scalar_type: &str) -> &'static str {
|
|
|
44
44
|
"String" => "as_str",
|
|
45
45
|
"Integer" => "as_i64",
|
|
46
46
|
"Float" => "as_f64",
|
|
47
|
+
"TrueClass" | "FalseClass" => "as_bool",
|
|
47
48
|
other => panic!("no JSON accessor for scalar type {other:?}"),
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -77,6 +78,7 @@ pub fn scalar_to_json_expr(scalar_type: &str, rust_expr: &str) -> String {
|
|
|
77
78
|
"String" => format!("crate::kernel::Json::Str({rust_expr}.clone())"),
|
|
78
79
|
"Integer" => format!("crate::kernel::Json::int({rust_expr})"),
|
|
79
80
|
"Float" => format!("crate::kernel::Json::Float({rust_expr})"),
|
|
81
|
+
"TrueClass" | "FalseClass" => format!("crate::kernel::Json::Bool({rust_expr})"),
|
|
80
82
|
other => panic!("no to_json expr for scalar type {other:?}"),
|
|
81
83
|
}
|
|
82
84
|
}
|
|
@@ -565,6 +567,7 @@ pub fn emit_closed_set_table_codec(exemplar: &Exemplar, vo: &Json) -> String {
|
|
|
565
567
|
Some("String") => format!("crate::kernel::Json::Str(self.{ident}.to_string())"),
|
|
566
568
|
Some("Integer") => format!("crate::kernel::Json::int(self.{ident})"),
|
|
567
569
|
Some("Float") => format!("crate::kernel::Json::Float(self.{ident})"),
|
|
570
|
+
Some("TrueClass" | "FalseClass") => format!("crate::kernel::Json::Bool(self.{ident})"),
|
|
568
571
|
_ => String::new(),
|
|
569
572
|
};
|
|
570
573
|
exemplar.render("to_json_field", &[("\"tmpl_field_name\"", naming::ruby_inspect_string(&key)), ("tmpl_json_value_placeholder()", value_expr)])
|