hecks 3.0.4 → 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 +25 -7
- 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/console.rb +24 -8
- 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 +69 -18
- 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 +38 -9
- 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 +23 -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 +8 -4
- 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 +7 -4
- 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/ports/persistence/plugins/era/postgres_era.rb +2 -1
- 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 +172 -95
- 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/command_interpreter.rb +2 -0
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/invocation.rb +5 -3
- 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/host/src/web/newsletter/template.rs +69 -0
- data/rust/host/src/web/newsletter/tests.rs +43 -0
- data/rust/host/src/web/newsletter.rs +6 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- 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 +39 -2
data/lib/hecks/doors/mcp_door.rb
CHANGED
|
@@ -266,30 +266,28 @@ module Hecks
|
|
|
266
266
|
end
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
-
# `domain:` is confined to `Storehouse::BOOT_ROOT`, and further to a
|
|
270
|
-
# door's `HECKS_DOOR_DOMAINS`, before anything boots.
|
|
269
|
+
# `domain:` is confined to `Storehouse::BOOT_ROOT`, and further to a restricted
|
|
270
|
+
# door's `HECKS_DOOR_DOMAINS`, before anything boots. An unrestricted door boots the
|
|
271
|
+
# domain afresh on every call; a restricted door lives for one spawner's session and
|
|
272
|
+
# keeps each named domain booted until the files of its directory change.
|
|
271
273
|
def boot(domain)
|
|
272
|
-
|
|
274
|
+
path = Storehouse.confine!(scope.admit_domain!(domain), "domain")
|
|
275
|
+
return Hecks.boot(path, install_doors: false) unless scope.restricted?
|
|
276
|
+
|
|
277
|
+
resident = (@resident ||= {})
|
|
278
|
+
fingerprint = Storehouse.fingerprint(path)
|
|
279
|
+
return resident[path][:runtime] if resident[path] && resident[path][:fingerprint] == fingerprint
|
|
280
|
+
|
|
281
|
+
resident[path] = { fingerprint: fingerprint, runtime: Hecks.boot(path, install_doors: false) }
|
|
282
|
+
resident[path][:runtime]
|
|
273
283
|
end
|
|
274
284
|
|
|
275
285
|
# One `when` per tool, so the shared `rescue` catches every branch's escape
|
|
276
286
|
# once; a domain refusal is already `{ok: false, ...}` from `Storehouse`.
|
|
277
287
|
def call_tool(name, args) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
278
288
|
case name
|
|
279
|
-
when "dispatch"
|
|
280
|
-
|
|
281
|
-
if args["steps"]
|
|
282
|
-
Storehouse.dispatch_batch(runtime: runtime, steps: args["steps"], summary: args["summary"],
|
|
283
|
-
source: args["source"], role: args["role"], actor_id: args["actor_id"])
|
|
284
|
-
else
|
|
285
|
-
Storehouse.dispatch(runtime: runtime, command: args["command"], summary: args["summary"],
|
|
286
|
-
args: args["args"] || {}, source: args["source"],
|
|
287
|
-
dry_run: args["dry_run"] == true, role: args["role"], actor_id: args["actor_id"])
|
|
288
|
-
end
|
|
289
|
-
when "query"
|
|
290
|
-
Storehouse.query(runtime: boot(args["domain"]), question: args["question"],
|
|
291
|
-
summary: args["summary"], args: args["args"] || {}, source: args["source"],
|
|
292
|
-
role: args["role"], actor_id: args["actor_id"])
|
|
289
|
+
when "dispatch" then dispatch_tool(args)
|
|
290
|
+
when "query" then query_tool(args)
|
|
293
291
|
when "events"
|
|
294
292
|
Storehouse.events(runtime: boot(args["domain"]), aggregate: args["aggregate"], id: args["id"],
|
|
295
293
|
limit: args["limit"])
|
|
@@ -319,6 +317,29 @@ module Hecks
|
|
|
319
317
|
{ ok: false, error: "#{e.class}: #{e.message}" }
|
|
320
318
|
end
|
|
321
319
|
|
|
320
|
+
# A question, once the scope has admitted its arguments.
|
|
321
|
+
def query_tool(args)
|
|
322
|
+
scope.admit_arguments!(args["args"])
|
|
323
|
+
Storehouse.query(runtime: boot(args["domain"]), question: args["question"],
|
|
324
|
+
summary: args["summary"], args: args["args"] || {}, source: args["source"],
|
|
325
|
+
role: args["role"], actor_id: args["actor_id"])
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# A single command or a batch, once the scope has admitted every command in it.
|
|
329
|
+
def dispatch_tool(args)
|
|
330
|
+
runtime = boot(args["domain"])
|
|
331
|
+
if args["steps"]
|
|
332
|
+
scope.admit_steps!(runtime, args["steps"])
|
|
333
|
+
Storehouse.dispatch_batch(runtime: runtime, steps: args["steps"], summary: args["summary"],
|
|
334
|
+
source: args["source"], role: args["role"], actor_id: args["actor_id"])
|
|
335
|
+
else
|
|
336
|
+
scope.admit_command!(runtime, args["command"], args["args"])
|
|
337
|
+
Storehouse.dispatch(runtime: runtime, command: args["command"], summary: args["summary"],
|
|
338
|
+
args: args["args"] || {}, source: args["source"],
|
|
339
|
+
dry_run: args["dry_run"] == true, role: args["role"], actor_id: args["actor_id"])
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
322
343
|
def answer_tool(name, args)
|
|
323
344
|
return scope.refusal(name) unless scope.permits_tool?(name)
|
|
324
345
|
|
|
@@ -352,7 +373,8 @@ module Hecks
|
|
|
352
373
|
when "notifications/initialized"
|
|
353
374
|
nil # a notification — no id, no response
|
|
354
375
|
when "tools/list"
|
|
355
|
-
|
|
376
|
+
served = TOOLS.select { |tool| scope.permits_tool?(tool[:name]) }
|
|
377
|
+
send_response(id, { tools: served.map { |tool| scope.present(tool) } })
|
|
356
378
|
when "tools/call"
|
|
357
379
|
send_response(id, tool_result(answer_tool(params["name"], params["arguments"] || {})))
|
|
358
380
|
when "ping"
|
|
@@ -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`
|