hecks 3.0.5 → 3.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.
- checksums.yaml +4 -4
- data/exe/hecks +24 -12
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +31 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +51 -29
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +11 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +52 -32
- data/lib/hecks/doors/launcher_options.rb +5 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +5 -5
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +45 -3
- data/lib/hecks/hecks/hecks.hecksagon +10 -4
- data/lib/hecks/hecks/hecks.world +6 -6
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/projections/deploy/box/settings.rb +248 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box.rb +387 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +122 -104
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- metadata +37 -1
|
@@ -2,27 +2,55 @@ require_relative "../runtime/errors"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Doors
|
|
5
|
-
# What a spawned hecks mcp may do: every tool by default
|
|
6
|
-
#
|
|
5
|
+
# What a spawned hecks mcp may do: every tool by default; only reader tools against named
|
|
6
|
+
# domains in reader mode; and in commands mode those plus `dispatch` of a closed list of
|
|
7
|
+
# commands — reach, not identity (ADR 0072, ADR 0089).
|
|
7
8
|
class McpDoorScope
|
|
8
|
-
ENV_PREFIX
|
|
9
|
-
TOOLS_VARIABLE
|
|
10
|
-
DOMAINS_VARIABLE
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
ENV_PREFIX = "HECKS_DOOR_".freeze
|
|
10
|
+
TOOLS_VARIABLE = "HECKS_DOOR_TOOLS".freeze
|
|
11
|
+
DOMAINS_VARIABLE = "HECKS_DOOR_DOMAINS".freeze
|
|
12
|
+
COMMANDS_VARIABLE = "HECKS_DOOR_COMMANDS".freeze
|
|
13
|
+
READERS = "readers".freeze
|
|
14
|
+
COMMANDS = "commands".freeze
|
|
15
|
+
MODES = [READERS, COMMANDS].freeze
|
|
16
|
+
EXIT_STATUS = 2
|
|
13
17
|
|
|
14
18
|
# The tools a reader door serves — each reads a domain or the door's own
|
|
15
19
|
# audit log and writes no domain state. `validate` still boots its
|
|
16
20
|
# domain, so it stays bound to the allowed set like every other tool.
|
|
17
21
|
READER_TOOLS = %w[query events state catalog describe validate domains history follow].freeze
|
|
18
22
|
|
|
23
|
+
# The tools a commands door serves: the reader tools, and `dispatch` for the allowed commands.
|
|
24
|
+
COMMAND_TOOLS = (READER_TOOLS + %w[dispatch]).freeze
|
|
25
|
+
|
|
26
|
+
# Argument names a restricted door never passes on, whatever command carries them: each one
|
|
27
|
+
# names a host or a URL, a binary to run, a place to write, a port to open, a store to
|
|
28
|
+
# switch to, or flips a command from a preview to a change of state.
|
|
29
|
+
DENIED_ARGUMENTS = %w[
|
|
30
|
+
adapter artifact confirm expected from gem_only header health_path host no_wait npm_local
|
|
31
|
+
npm_only output path payload payload_file port rust_binary scheme state_path stdio url write
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Argument names whose values are paths (a comma-separated list for some). A restricted door
|
|
35
|
+
# passes them on only when each resolves, symlinks followed, inside `Storehouse::BOOT_ROOT`,
|
|
36
|
+
# and holds no colon: `host:repo` and `https://host/x` would otherwise pass as relative paths.
|
|
37
|
+
PATH_ARGUMENTS = %w[dir domain domains file fixture paths root script].freeze
|
|
38
|
+
|
|
39
|
+
# Argument names whose values are git refs: plain names only, so a value cannot read as an
|
|
40
|
+
# option to git.
|
|
41
|
+
REF_ARGUMENTS = %w[ref].freeze
|
|
42
|
+
|
|
43
|
+
# What a plain git ref looks like: no leading dash, no `..`, no spaces.
|
|
44
|
+
PLAIN_REF = %r{\A[A-Za-z0-9][A-Za-z0-9._/~^@-]*\z}
|
|
45
|
+
|
|
19
46
|
# Unrestricted when no HECKS_DOOR_* variable is set.
|
|
20
47
|
def self.from_env(env = ENV)
|
|
21
48
|
problems = setting_problems(env)
|
|
22
49
|
raise ArgumentError, problems.join("\n") unless problems.empty?
|
|
23
50
|
return new(allowed_domains: nil) unless env[TOOLS_VARIABLE]
|
|
24
51
|
|
|
25
|
-
|
|
52
|
+
commands = env[TOOLS_VARIABLE] == COMMANDS ? split_names(env[COMMANDS_VARIABLE]) : nil
|
|
53
|
+
new(allowed_domains: split_paths(env[DOMAINS_VARIABLE]), allowed_commands: commands)
|
|
26
54
|
end
|
|
27
55
|
|
|
28
56
|
# Refuses to stderr, not stdout, which carries the MCP protocol.
|
|
@@ -34,44 +62,80 @@ module Hecks
|
|
|
34
62
|
end
|
|
35
63
|
|
|
36
64
|
def self.setting_problems(env)
|
|
37
|
-
|
|
38
|
-
problems =
|
|
39
|
-
"environment #{name} is not accepted; the door takes only #{
|
|
65
|
+
known = [TOOLS_VARIABLE, DOMAINS_VARIABLE, COMMANDS_VARIABLE]
|
|
66
|
+
problems = (env.keys.select { |name| name.start_with?(ENV_PREFIX) } - known).map do |name|
|
|
67
|
+
"environment #{name} is not accepted; the door takes only #{known.join(', ')}"
|
|
40
68
|
end
|
|
41
|
-
problems + mode_problems(env[TOOLS_VARIABLE], env[DOMAINS_VARIABLE])
|
|
69
|
+
problems + mode_problems(env[TOOLS_VARIABLE], env[DOMAINS_VARIABLE], env[COMMANDS_VARIABLE])
|
|
42
70
|
end
|
|
43
71
|
|
|
44
|
-
def self.mode_problems(tools, domains)
|
|
45
|
-
if tools.nil?
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
def self.mode_problems(tools, domains, commands)
|
|
73
|
+
return stray_problems(domains, commands) if tools.nil?
|
|
74
|
+
unless MODES.include?(tools)
|
|
75
|
+
return ["#{TOOLS_VARIABLE}=#{tools.inspect} is not accepted; the values are #{MODES.map(&:inspect).join(' and ')}"]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
problems = []
|
|
79
|
+
if split_paths(domains.to_s).empty?
|
|
80
|
+
problems << "#{TOOLS_VARIABLE}=#{tools} needs #{DOMAINS_VARIABLE}, the domains this door may read"
|
|
81
|
+
end
|
|
82
|
+
problems + command_problems(tools, commands)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.stray_problems(domains, commands)
|
|
86
|
+
stray = { DOMAINS_VARIABLE => domains, COMMANDS_VARIABLE => commands }.compact.keys
|
|
87
|
+
return [] if stray.empty?
|
|
88
|
+
|
|
89
|
+
["#{stray.join(' and ')} set without #{TOOLS_VARIABLE}=#{READERS} or #{TOOLS_VARIABLE}=#{COMMANDS}; " \
|
|
90
|
+
"they only narrow a restricted door"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.command_problems(tools, commands)
|
|
94
|
+
if tools == COMMANDS && split_names(commands).empty?
|
|
95
|
+
["#{TOOLS_VARIABLE}=#{COMMANDS} needs #{COMMANDS_VARIABLE}, the commands this door may dispatch"]
|
|
96
|
+
elsif tools == READERS && !commands.nil?
|
|
97
|
+
["#{COMMANDS_VARIABLE} is set in reader mode; it only narrows a commands door"]
|
|
51
98
|
else
|
|
52
99
|
[]
|
|
53
100
|
end
|
|
54
101
|
end
|
|
55
102
|
|
|
103
|
+
def self.split_paths(value) = value.to_s.split(File::PATH_SEPARATOR).reject(&:empty?)
|
|
104
|
+
|
|
105
|
+
def self.split_names(value) = value.to_s.split(",").map(&:strip).reject(&:empty?).uniq
|
|
106
|
+
|
|
56
107
|
# The named domains, resolved against Storehouse::BOOT_ROOT; nil when unrestricted.
|
|
57
108
|
attr_reader :allowed_domains
|
|
58
109
|
|
|
59
|
-
|
|
110
|
+
# The command names a commands door dispatches, as the spawner wrote them; nil unless
|
|
111
|
+
# the door runs in commands mode.
|
|
112
|
+
attr_reader :allowed_commands
|
|
113
|
+
|
|
114
|
+
def initialize(allowed_domains:, allowed_commands: nil)
|
|
60
115
|
@allowed_domains = allowed_domains&.map { |path| Storehouse.confine!(path, DOMAINS_VARIABLE) }.freeze
|
|
116
|
+
@allowed_commands = allowed_commands&.dup&.freeze
|
|
61
117
|
end
|
|
62
118
|
|
|
63
119
|
def restricted?
|
|
64
120
|
!@allowed_domains.nil?
|
|
65
121
|
end
|
|
66
122
|
|
|
123
|
+
def commands_mode?
|
|
124
|
+
!@allowed_commands.nil?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def served_tools
|
|
128
|
+
commands_mode? ? COMMAND_TOOLS : READER_TOOLS
|
|
129
|
+
end
|
|
130
|
+
|
|
67
131
|
def permits_tool?(name)
|
|
68
|
-
!restricted? ||
|
|
132
|
+
!restricted? || served_tools.include?(name)
|
|
69
133
|
end
|
|
70
134
|
|
|
71
135
|
def refusal(name)
|
|
72
136
|
{ ok: false,
|
|
73
|
-
error: "#{name.inspect} is refused: this door runs in
|
|
74
|
-
"which serves only #{
|
|
137
|
+
error: "#{name.inspect} is refused: this door runs in #{mode_label}, " \
|
|
138
|
+
"which serves only #{served_tools.join(', ')}" }
|
|
75
139
|
end
|
|
76
140
|
|
|
77
141
|
# Checked before anything boots, so a path outside the allowed set never reaches Kernel.load.
|
|
@@ -80,18 +144,174 @@ module Hecks
|
|
|
80
144
|
return domain if @allowed_domains.include?(Storehouse.confine!(domain, "domain"))
|
|
81
145
|
|
|
82
146
|
raise Runtime::TypeMismatch,
|
|
83
|
-
"domain: #{domain.inspect} is refused: this door runs in
|
|
147
|
+
"domain: #{domain.inspect} is refused: this door runs in #{mode_label} " \
|
|
84
148
|
"and reads only #{DOMAINS_VARIABLE}: #{@allowed_domains.join(', ')}"
|
|
85
149
|
end
|
|
86
150
|
|
|
151
|
+
# A commands door dispatches only the commands it was given. A requested name and each
|
|
152
|
+
# allowed name resolve through the alias map `dispatch` uses, and the resolved verbs are
|
|
153
|
+
# compared, so `complete` cannot reach a command of another aggregate that shares its short
|
|
154
|
+
# name, and an allowed name that resolves to nothing admits nothing.
|
|
155
|
+
#
|
|
156
|
+
# @param runtime [Runtime::Dispatcher] the booted domain
|
|
157
|
+
# @param command [String, nil] the command name the caller asked for
|
|
158
|
+
# @param args [Hash, nil] the arguments the caller gave it; see `admit_arguments!`
|
|
159
|
+
# @raise [Runtime::TypeMismatch] when the command is not on the list or an argument is refused
|
|
160
|
+
def admit_command!(runtime, command, args = nil)
|
|
161
|
+
return unless commands_mode?
|
|
162
|
+
|
|
163
|
+
verbs = Storehouse.verbs_for(runtime, [command, *allowed_commands])
|
|
164
|
+
unless verbs.first && verbs.drop(1).include?(verbs.first)
|
|
165
|
+
raise Runtime::TypeMismatch,
|
|
166
|
+
"command: #{command.to_s.inspect} is refused: this door runs in #{mode_label} " \
|
|
167
|
+
"and dispatches only #{COMMANDS_VARIABLE}: #{allowed_commands.join(', ')}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
admit_arguments!(args)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Every step of a batch is admitted before any step runs.
|
|
174
|
+
#
|
|
175
|
+
# @param runtime [Runtime::Dispatcher] the booted domain
|
|
176
|
+
# @param steps [Array<Hash>] the batch, each `{"command" => ..., "args" => ...}`
|
|
177
|
+
# @raise [Runtime::TypeMismatch] when a step names a command off the list or a bad argument
|
|
178
|
+
def admit_steps!(runtime, steps)
|
|
179
|
+
Array(steps).each do |step|
|
|
180
|
+
step = {} unless step.is_a?(Hash)
|
|
181
|
+
admit_command!(runtime, step["command"], step["args"])
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The arguments a restricted door is about to pass to a command or a question. A command
|
|
186
|
+
# list admits commands, not values, so each argument is checked by its name: a denied name
|
|
187
|
+
# is refused, a path must resolve inside the root with symlinks followed, and a git ref must
|
|
188
|
+
# be a plain name. Other names pass; a domain with an argument that reaches outside its own
|
|
189
|
+
# files should name it in `DENIED_ARGUMENTS` or `PATH_ARGUMENTS` (ADR 0089).
|
|
190
|
+
#
|
|
191
|
+
# @param args [Hash, Array, nil] the arguments as the caller gave them, nested or not
|
|
192
|
+
# @raise [Runtime::TypeMismatch] on the first argument refused
|
|
193
|
+
def admit_arguments!(args)
|
|
194
|
+
return unless restricted?
|
|
195
|
+
|
|
196
|
+
each_argument(args) do |name, value|
|
|
197
|
+
refuse_argument!(name, "never passes it on: it names a host, a binary, an output or a change of state") if
|
|
198
|
+
DENIED_ARGUMENTS.include?(name)
|
|
199
|
+
admit_path!(name, value) if PATH_ARGUMENTS.include?(name)
|
|
200
|
+
admit_ref!(name, value) if REF_ARGUMENTS.include?(name)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The tool as `tools/list` shows it: on a commands door, `dispatch` names the commands it
|
|
205
|
+
# serves, so a caller sees them as an enum instead of finding out by being refused.
|
|
206
|
+
#
|
|
207
|
+
# @param tool [Hash] a tool definition from `McpDoor::TOOLS`
|
|
208
|
+
# @return [Hash] the definition, narrowed for this door
|
|
209
|
+
def present(tool)
|
|
210
|
+
return tool unless commands_mode? && tool[:name] == "dispatch"
|
|
211
|
+
|
|
212
|
+
properties = tool[:inputSchema][:properties]
|
|
213
|
+
narrowed = properties.merge(command: with_enum(properties[:command]),
|
|
214
|
+
steps: properties[:steps].merge(items: step_items(properties[:steps][:items])))
|
|
215
|
+
tool.merge(description: "#{tool[:description]} This door dispatches only: #{allowed_commands.join(', ')}.",
|
|
216
|
+
inputSchema: tool[:inputSchema].merge(properties: narrowed))
|
|
217
|
+
end
|
|
218
|
+
|
|
87
219
|
def notes
|
|
88
220
|
return [] unless restricted?
|
|
89
221
|
|
|
222
|
+
commands_mode? ? commands_notes : reader_notes
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
# Yields every argument name with its value, descending into nested objects and lists, so
|
|
228
|
+
# `{"file" => {"value" => "x"}}` is checked the way `{"file" => "x"}` is.
|
|
229
|
+
def each_argument(args, &visit)
|
|
230
|
+
case args
|
|
231
|
+
when Hash
|
|
232
|
+
args.each do |name, value|
|
|
233
|
+
visit.call(name.to_s, value)
|
|
234
|
+
each_argument(value, &visit)
|
|
235
|
+
end
|
|
236
|
+
when Array
|
|
237
|
+
args.each { |item| each_argument(item, &visit) }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# A value written as an object of one field (`{"value" => "x"}`) is that field's value.
|
|
242
|
+
def scalar(value)
|
|
243
|
+
return value unless value.is_a?(Hash)
|
|
244
|
+
|
|
245
|
+
value.fetch("value") { value.fetch(:value, value) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def refuse_argument!(name, why)
|
|
249
|
+
raise Runtime::TypeMismatch, "argument: #{name.inspect} is refused: this door runs in #{mode_label} and #{why}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def admit_path!(name, value)
|
|
253
|
+
Array(scalar(value)).flat_map { |item| item.to_s.split(",") }.reject(&:empty?).each do |path|
|
|
254
|
+
next if !path.include?(":") && inside_root?(path)
|
|
255
|
+
|
|
256
|
+
refuse_argument!(name, "passes a path only when it resolves inside #{Storehouse::BOOT_ROOT} " \
|
|
257
|
+
"and holds no colon: #{path.inspect}")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def admit_ref!(name, value)
|
|
262
|
+
ref = scalar(value).to_s
|
|
263
|
+
return if ref.match?(PLAIN_REF) && !ref.include?("..")
|
|
264
|
+
|
|
265
|
+
refuse_argument!(name, "passes only a plain git ref: #{ref.inspect}")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def inside_root?(path)
|
|
269
|
+
root = resolve_real(Storehouse::BOOT_ROOT)
|
|
270
|
+
resolved = resolve_real(File.expand_path(path, Storehouse::BOOT_ROOT))
|
|
271
|
+
resolved == root || resolved.start_with?("#{root}#{File::SEPARATOR}")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The real path of `path`, symlinks followed: of the whole path when it exists, else of its
|
|
275
|
+
# deepest existing parent with the missing names put back, so a link above a file that does
|
|
276
|
+
# not exist yet still shows where the file would land.
|
|
277
|
+
def resolve_real(path)
|
|
278
|
+
missing = []
|
|
279
|
+
current = File.expand_path(path)
|
|
280
|
+
until File.exist?(current)
|
|
281
|
+
parent = File.dirname(current)
|
|
282
|
+
break if parent == current
|
|
283
|
+
|
|
284
|
+
missing.unshift(File.basename(current))
|
|
285
|
+
current = parent
|
|
286
|
+
end
|
|
287
|
+
File.join(File.realpath(current), *missing)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def with_enum(property)
|
|
291
|
+
property.merge(enum: allowed_commands)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def step_items(items)
|
|
295
|
+
items.merge(properties: items[:properties].merge(command: with_enum(items[:properties][:command])))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def mode_label
|
|
299
|
+
commands_mode? ? "commands mode (#{TOOLS_VARIABLE}=#{COMMANDS})" : "reader mode (#{TOOLS_VARIABLE}=#{READERS})"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def reader_notes
|
|
90
303
|
["Reader mode (#{TOOLS_VARIABLE}=#{READERS}): serves #{READER_TOOLS.join(', ')}; " \
|
|
91
304
|
"refuses dispatch (with dry_run and steps), behaviors and every other tool.",
|
|
92
305
|
"domain: boots only #{DOMAINS_VARIABLE}: #{@allowed_domains.join(', ')}.",
|
|
93
306
|
"Reader mode limits reach and identifies no one; it is not authentication."]
|
|
94
307
|
end
|
|
308
|
+
|
|
309
|
+
def commands_notes
|
|
310
|
+
["Commands mode (#{TOOLS_VARIABLE}=#{COMMANDS}): serves #{READER_TOOLS.join(', ')} and dispatch of only " \
|
|
311
|
+
"#{COMMANDS_VARIABLE}: #{@allowed_commands.join(', ')}; refuses behaviors and every other tool.",
|
|
312
|
+
"domain: boots only #{DOMAINS_VARIABLE}: #{@allowed_domains.join(', ')}.",
|
|
313
|
+
"Commands mode limits reach and identifies no one; role: stays self-asserted and it is not authentication."]
|
|
314
|
+
end
|
|
95
315
|
end
|
|
96
316
|
end
|
|
97
317
|
end
|
data/lib/hecks/doors.rb
CHANGED
|
@@ -24,12 +24,12 @@ module Hecks
|
|
|
24
24
|
def self.load!(name, registry: Hecks.current_registry)
|
|
25
25
|
unless registry&.root
|
|
26
26
|
raise Runtime::WiringError,
|
|
27
|
-
"
|
|
27
|
+
"attaches(#{name.inspect}, from: :vendor) needs a registry with a root to vendor from"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
unless name.to_s.match?(PACKAGE_NAME)
|
|
31
31
|
raise Runtime::WiringError,
|
|
32
|
-
"
|
|
32
|
+
"attaches(#{name.inspect}, from: :vendor) is not a package name — it must match " \
|
|
33
33
|
"[a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -37,7 +37,7 @@ module Hecks
|
|
|
37
37
|
Hecks::Adapters::Folder.new.load_bluebooks(File.join(root, "examples/banking/bluebook"))
|
|
38
38
|
Kernel.load(File.join(root, "lib/hecks/forms/examples/banking_console.bluebook"))
|
|
39
39
|
Hecks.hecksagon("Banking") do
|
|
40
|
-
|
|
40
|
+
attaches "Governance"
|
|
41
41
|
AGGREGATES.each { |name| ::Banking.const_get(name).persisted_by("Memory") }
|
|
42
42
|
end
|
|
43
43
|
Hecks.hecksagon("Governance") do
|
|
@@ -2,7 +2,7 @@ Hecks.bluebook "Compliance" do
|
|
|
2
2
|
vision "Something elsewhere already acted to contain a risk; this domain tracks the human review that decides what happens next."
|
|
3
3
|
supporting
|
|
4
4
|
|
|
5
|
-
# Supports both modes from one source: `
|
|
5
|
+
# Supports both modes from one source: `attaches "Compliance"` merges it in-process,
|
|
6
6
|
# or a separately deployed service is reached with `across "Compliance"` on a policy.
|
|
7
7
|
# examples/compliance/bluebook/compliance.bluebook symlinks here, since a symlink pointing
|
|
8
8
|
# outside lib/ does not survive `gem build`.
|
|
@@ -3,7 +3,7 @@ Hecks.bluebook "ConsoleSettings" do
|
|
|
3
3
|
supporting
|
|
4
4
|
|
|
5
5
|
# A framework member like Identity/Governance: attach with
|
|
6
|
-
# `
|
|
6
|
+
# `attaches "ConsoleSettings"` in the consuming app's own `.hecksagon`,
|
|
7
7
|
# plus a `Hecks.hecksagon "ConsoleSettings" do ... end` persistence binding.
|
|
8
8
|
|
|
9
9
|
# Named ConsoleSettings, not "Forms" — Hecks::Forms (lib/hecks/forms/) names a
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# The framework members bound to Memory so they can boot standing alone with
|
|
2
|
-
# `Hecks.boot("lib/hecks/framework")`. `
|
|
2
|
+
# `Hecks.boot("lib/hecks/framework")`. `attaches` does not read this file.
|
|
3
3
|
Hecks.hecksagon "Compliance" do
|
|
4
4
|
# Every command declares `role "Compliance officer"`; the role check needs Governance.
|
|
5
|
-
|
|
5
|
+
attaches "Governance"
|
|
6
6
|
persisted_by "Memory"
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
Hecks.hecksagon "ConsoleSettings" do
|
|
10
10
|
# Same reason as Compliance.
|
|
11
|
-
|
|
11
|
+
attaches "Governance"
|
|
12
12
|
persisted_by "Memory"
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -18,6 +18,6 @@ end
|
|
|
18
18
|
|
|
19
19
|
Hecks.hecksagon "Identity" do
|
|
20
20
|
# `Identity.Register` declares `role "Identity registrar"`; same reason as Compliance.
|
|
21
|
-
|
|
21
|
+
attaches "Governance"
|
|
22
22
|
persisted_by "Memory"
|
|
23
23
|
end
|
data/lib/hecks/framework.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Hecks
|
|
|
55
55
|
# Loads a member's bluebook unless this registry already holds it.
|
|
56
56
|
#
|
|
57
57
|
# `Kernel.load` re-executes the file, and a second `Declare` of the same aggregate is
|
|
58
|
-
# `AlreadyExists`, so repeated `
|
|
58
|
+
# `AlreadyExists`, so repeated `attaches` calls in one boot must skip.
|
|
59
59
|
#
|
|
60
60
|
# @param name [String, Symbol] the member's name, such as `"Governance"`
|
|
61
61
|
# @return [Boolean, nil] true when this call loaded the member's bluebook, nil when
|
|
@@ -5,7 +5,7 @@ require_relative "form_census"
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Fuzzing
|
|
7
7
|
# Asks an agent to write candidate bluebooks from the corpus's form-pair census.
|
|
8
|
-
# Opt-in: `hecks quality_control tick` never runs it; an agent call is costly and
|
|
8
|
+
# Opt-in: `hecks quality_control sweep.tick` never runs it; an agent call is costly and
|
|
9
9
|
# nondeterministic.
|
|
10
10
|
module CombinationMiner
|
|
11
11
|
PROMPT_TEMPLATE = "qa/combination_miner/prompt.md".freeze
|
|
@@ -55,7 +55,7 @@ module Hecks
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Filters to verbs whose aggregate resolves to a `:cross_process_lock`-declaring
|
|
58
|
-
# adapter — a Memory-backed aggregate (e.g. one only `
|
|
58
|
+
# adapter — a Memory-backed aggregate (e.g. one only `attaches`s) can never
|
|
59
59
|
# agree with the sequential oracle across two processes, a guaranteed false positive.
|
|
60
60
|
def lockable_verbs(domain_path, steps, database:, schema:)
|
|
61
61
|
verbs = steps.select { |step| COMMAND_STEP.call(step) }.map { |step| step["verb"] }.uniq
|
|
@@ -4,12 +4,14 @@ require_relative "replay"
|
|
|
4
4
|
require_relative "properties"
|
|
5
5
|
require_relative "self_consistency"
|
|
6
6
|
require_relative "rust_gap_manifest"
|
|
7
|
+
require_relative "../rust_build/kernel_input"
|
|
7
8
|
require_relative "nondeterministic"
|
|
8
9
|
|
|
9
10
|
module Hecks
|
|
10
11
|
module Fuzzing
|
|
11
12
|
# The Ruby-vs-Rust comparison of one generated sequence, shared by the QualityControl sweep
|
|
12
|
-
# (`hecks quality_control
|
|
13
|
+
# (`hecks quality_control query sweep.run`) and `hecks quality_control
|
|
14
|
+
# target.check_generated_domains --rust`.
|
|
13
15
|
#
|
|
14
16
|
# `differ` is duck-typed (the `RustConformanceHelpers` comparison helpers plus a
|
|
15
17
|
# `structural_skips` set) so lib never requires spec/.
|
|
@@ -91,7 +93,7 @@ module Hecks
|
|
|
91
93
|
outcomes[:adapter_parity_sqlite] = adapter_parity_sqlite.call
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
stdout, status = Open3.capture2(binary, stdin_data:
|
|
96
|
+
stdout, status = Open3.capture2(binary, stdin_data: RustBuild::KernelInput.json(domain_path, steps))
|
|
95
97
|
unless status.success?
|
|
96
98
|
return outcomes.merge(differential: [{ field: "process",
|
|
97
99
|
detail: "rust binary exited #{status.exitstatus}: #{stdout}" }])
|
|
@@ -9,7 +9,7 @@ module Hecks
|
|
|
9
9
|
|
|
10
10
|
# `kind:` on a `checked: false` result distinguishes a target with no
|
|
11
11
|
# lineage to audit (`:not_applicable`) from an audit that failed to
|
|
12
|
-
# run (`:error`) — `hecks quality_control
|
|
12
|
+
# run (`:error`) — `hecks quality_control query sweep.run` holds the first and surfaces the
|
|
13
13
|
# second as a finding, instead of treating both as a clean audit.
|
|
14
14
|
def diverged_ancestor_writes(domain_path)
|
|
15
15
|
registry, directory = load_registry(domain_path)
|
|
@@ -5,8 +5,8 @@ module Hecks
|
|
|
5
5
|
module Fuzzing
|
|
6
6
|
# The declared forms an aggregate can exhibit, and the pairs it puts together.
|
|
7
7
|
#
|
|
8
|
-
# Shared by the combination-coverage spec and `hecks quality_control judge_novelty` so
|
|
9
|
-
# cannot drift. The unit is one aggregate: forms on one head meet at dispatch, forms in one
|
|
8
|
+
# Shared by the combination-coverage spec and `hecks quality_control target.judge_novelty` so
|
|
9
|
+
# they cannot drift. The unit is one aggregate: forms on one head meet at dispatch, forms in one
|
|
10
10
|
# chapter do not.
|
|
11
11
|
module FormCensus
|
|
12
12
|
# A given path crossing two references has at least this many segments.
|
|
@@ -7,7 +7,7 @@ require_relative "differential"
|
|
|
7
7
|
|
|
8
8
|
module Hecks
|
|
9
9
|
module Fuzzing
|
|
10
|
-
# The child-process half of `hecks quality_control check_generated_domains`: checks one
|
|
10
|
+
# The child-process half of `hecks quality_control target.check_generated_domains`: checks one
|
|
11
11
|
# generated domain.
|
|
12
12
|
#
|
|
13
13
|
# One domain per process, since every generated domain is named `QaGenerated`.
|
|
@@ -9,6 +9,12 @@ module Hecks
|
|
|
9
9
|
# Rewrites a copied domain's persistence bindings before an ephemeral
|
|
10
10
|
# fuzz/replay boot; a directory copy alone does not isolate Postgres state.
|
|
11
11
|
module IsolatedBoot
|
|
12
|
+
# A vendored package named by `attaches "name", from: :vendor`.
|
|
13
|
+
VENDORED_ATTACHED = /attaches\s*\(?\s*"([\w-]+)"\s*\)?\s*,\s*from:\s*:vendor/
|
|
14
|
+
|
|
15
|
+
# A vendored package named by the deprecated `uses_embryonaut_bluebook "name"`.
|
|
16
|
+
VENDORED_DEPRECATED = /uses_embryonaut_bluebook\s*\(?\s*"([\w-]+)"/
|
|
17
|
+
|
|
12
18
|
module_function
|
|
13
19
|
|
|
14
20
|
# Copies `domain_path` to a tmpdir, rebinds its persistence to `adapter`, and
|
|
@@ -67,7 +73,8 @@ module Hecks
|
|
|
67
73
|
# `Hecks.boot` only sees the domain directory itself, not `vendor/`.
|
|
68
74
|
def carry_vendored_bluebooks!(source, destination)
|
|
69
75
|
names = Dir.glob(File.join(destination, "**", "*.hecksagon")).flat_map do |path|
|
|
70
|
-
File.read(path)
|
|
76
|
+
text = File.read(path)
|
|
77
|
+
(text.scan(VENDORED_DEPRECATED) + text.scan(VENDORED_ATTACHED)).flatten
|
|
71
78
|
end
|
|
72
79
|
names.uniq.each do |name|
|
|
73
80
|
package = File.join(File.dirname(source), "vendor", "embryonaut_bluebooks", name)
|
|
@@ -161,8 +168,8 @@ module Hecks
|
|
|
161
168
|
# `:postgres_era` is the only mode that exercises era/lineage-bound SQL; plain
|
|
162
169
|
# `:postgres` never touches that machinery. Unlike `:postgres`, there's no shared
|
|
163
170
|
# scratch constant here — `database:`/`schema:` are required keyword args because
|
|
164
|
-
# the caller (`hecks quality_control
|
|
165
|
-
# lifecycle.
|
|
171
|
+
# the caller (`hecks quality_control query sweep.run --persistence-parity`) owns that
|
|
172
|
+
# database's lifecycle.
|
|
166
173
|
def rebind_to_postgres_era!(copy, database:, schema:)
|
|
167
174
|
require "pg"
|
|
168
175
|
if database.to_s.empty? || schema.to_s.empty?
|
|
@@ -11,8 +11,8 @@ module Hecks
|
|
|
11
11
|
|
|
12
12
|
# Replays `steps` on both adapters and lists where the results differ.
|
|
13
13
|
#
|
|
14
|
-
# Compares the same six fields as `hecks quality_control
|
|
15
|
-
# results are JSON-round-tripped so plain data compares equal on both sides.
|
|
14
|
+
# Compares the same six fields as `hecks quality_control query sweep.run`'s Ruby-vs-Rust mode.
|
|
15
|
+
# Both results are JSON-round-tripped so plain data compares equal on both sides.
|
|
16
16
|
#
|
|
17
17
|
# @param left [Symbol] an `IsolatedBoot` adapter: `:memory`, `:sqlite`, `:postgres`,
|
|
18
18
|
# `:postgres_era`
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
POSTGRES_ERA_VARIABLE = /^\s*([a-z_]\w*)\s*=\s*"PostgresEra"\s*(?:#.*)?$/
|
|
17
17
|
|
|
18
18
|
# Captures the member a hecksagon attaches; authorization is read off its declaration.
|
|
19
|
-
FRAMEWORK_ATTACHED = /uses_framework\s*\(?\s*"([^"]+)"/
|
|
19
|
+
FRAMEWORK_ATTACHED = /(?:attaches|uses_framework)\s*\(?\s*"([^"]+)"/
|
|
20
20
|
|
|
21
21
|
# A command-level `role "..."`, the only construct a role check can compare a caller against.
|
|
22
22
|
ROLE_GATED = /^\s*role\s+"/
|
|
@@ -41,8 +41,8 @@ module Hecks
|
|
|
41
41
|
wasm_front: %w[rust]
|
|
42
42
|
}.freeze
|
|
43
43
|
|
|
44
|
-
# The modes `hecks quality_control
|
|
45
|
-
# resolved.
|
|
44
|
+
# The modes `hecks quality_control query sweep.run` can run; one absent here is refused at
|
|
45
|
+
# start, not resolved.
|
|
46
46
|
RUNNABLE_MODES = %i[differential ruby_only self_consistency properties_in_differential
|
|
47
47
|
structural_skip_report adapter_parity_sqlite persistence_parity
|
|
48
48
|
era_boundary concurrency].freeze
|
data/lib/hecks/gate/stages.yml
CHANGED
|
@@ -27,7 +27,7 @@ pre_push:
|
|
|
27
27
|
blocked: the engines disagree about a declared query; one of them is wrong, and a self-consistent adapter spec cannot tell which.
|
|
28
28
|
- id: engine_agreement
|
|
29
29
|
title: engine agreement, every declared comparator on both engines
|
|
30
|
-
run: [bundle, exec, exe/hecks, check_engine_agreement, --wait]
|
|
30
|
+
run: [bundle, exec, exe/hecks, conformance_run.check_engine_agreement, --wait]
|
|
31
31
|
blocked: a comparator is missing a shared case or a cross-engine spec.
|
|
32
32
|
- id: model_check
|
|
33
33
|
title: model check, unreachable states, dead transitions and orphaned handlers
|
|
@@ -35,7 +35,7 @@ pre_push:
|
|
|
35
35
|
blocked: the model checker found something.
|
|
36
36
|
- id: doc_coverage
|
|
37
37
|
title: docs coverage, words with no running example
|
|
38
|
-
run: [bundle, exec, exe/hecks, measure_doc_coverage, --wait]
|
|
38
|
+
run: [bundle, exec, exe/hecks, conformance_run.measure_doc_coverage, --wait]
|
|
39
39
|
blocked: the language ships something undocumented; write the missing sections and examples.
|
|
40
40
|
- id: rubocop
|
|
41
41
|
title: rubocop, the static style and correctness gate
|
|
@@ -43,13 +43,13 @@ pre_push:
|
|
|
43
43
|
blocked: rubocop found an offense.
|
|
44
44
|
- id: codegen_drift
|
|
45
45
|
title: codegen drift, the committed rust/src/generated/ must match a fresh regeneration
|
|
46
|
-
run: [bundle, exec, exe/hecks, regenerate_corpus, --check, --wait]
|
|
46
|
+
run: [bundle, exec, exe/hecks, regeneration_run.regenerate_corpus, --check, --wait]
|
|
47
47
|
blocked: regenerating hits a diff, so the committed Rust is stale; run `hecks regenerate_corpus --confirm` and commit the result.
|
|
48
48
|
- id: comment_style
|
|
49
49
|
title: comment style, every comment under lib/hecks against docs/COMMENT_STYLE_GUIDE.md
|
|
50
|
-
run: [bundle, exec, exe/hecks, check_comments, paths=lib/hecks, --wait]
|
|
50
|
+
run: [bundle, exec, exe/hecks, style_run.check_comments, paths=lib/hecks, --wait]
|
|
51
51
|
blocked: a comment breaks the style guide, such as a line past 100 characters; `hecks fix_comments` rewrites the mechanical ones, and the rest are shortened by hand. CI runs this same check.
|
|
52
52
|
- id: comment_blocks
|
|
53
53
|
title: comment blocks, no run of comment lines past ADR 0075's cap
|
|
54
|
-
run: [bundle, exec, exe/hecks, check_comments, "paths=lib,spec,examples", only=long_block, --wait]
|
|
54
|
+
run: [bundle, exec, exe/hecks, style_run.check_comments, "paths=lib,spec,examples", only=long_block, --wait]
|
|
55
55
|
blocked: a comment block ran past the cap; shorten it.
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
Hecks.hecksagon "Expression" do
|
|
5
5
|
# Every command here declares `role "Language designer"` — only real
|
|
6
6
|
# access control once Governance can check it against something (S8).
|
|
7
|
-
|
|
7
|
+
attaches "Governance"
|
|
8
8
|
persisted_by "Memory"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
Hecks.hecksagon "Translation" do
|
|
12
12
|
# Same reason as Expression, above — S8.
|
|
13
|
-
|
|
13
|
+
attaches "Governance"
|
|
14
14
|
persisted_by "Memory"
|
|
15
15
|
end
|
|
16
16
|
|