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.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +24 -12
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +31 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  12. data/lib/hecks/cli/doc_coverage.rb +1 -1
  13. data/lib/hecks/cli/domain_stub.rb +128 -0
  14. data/lib/hecks/cli/interview_draft.rb +246 -0
  15. data/lib/hecks/cli/model_check.rb +1 -1
  16. data/lib/hecks/cli/project_cli.rb +51 -29
  17. data/lib/hecks/cli/reference.rb +2 -1
  18. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  19. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  20. data/lib/hecks/cli/spec_example.rb +1 -1
  21. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  22. data/lib/hecks/corpus.rb +11 -8
  23. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  24. data/lib/hecks/deploy/oidc.json +5 -0
  25. data/lib/hecks/doc/reference.rb +4 -3
  26. data/lib/hecks/doors/cli_door.rb +13 -13
  27. data/lib/hecks/doors/cli_runner.rb +52 -32
  28. data/lib/hecks/doors/launcher_options.rb +5 -5
  29. data/lib/hecks/doors/mcp_door.rb +40 -18
  30. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  31. data/lib/hecks/doors.rb +1 -1
  32. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  33. data/lib/hecks/forms/banking_presentation.rb +1 -1
  34. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  35. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  36. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  37. data/lib/hecks/framework.rb +1 -1
  38. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  39. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  40. data/lib/hecks/fuzzing/differential.rb +4 -2
  41. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  42. data/lib/hecks/fuzzing/form_census.rb +2 -2
  43. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  44. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  45. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  46. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  47. data/lib/hecks/gate/stages.yml +5 -5
  48. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  49. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  50. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  51. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  52. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  53. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  54. data/lib/hecks/hecks/codebase.bluebook +4 -3
  55. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  56. data/lib/hecks/hecks/custodian.bluebook +45 -3
  57. data/lib/hecks/hecks/hecks.hecksagon +10 -4
  58. data/lib/hecks/hecks/hecks.world +6 -6
  59. data/lib/hecks/hecks/site.behaviors +17 -0
  60. data/lib/hecks/hecks/site.world +10 -0
  61. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  62. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  63. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  64. data/lib/hecks/projections/deploy/box/settings.rb +248 -0
  65. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
  66. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
  67. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  68. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
  69. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  70. data/lib/hecks/projections/deploy/box.rb +387 -0
  71. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  72. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  73. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  74. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  75. data/lib/hecks/projections/diagrams.rb +2 -2
  76. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  77. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  78. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  79. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  80. data/lib/hecks/projections/site/edge.rb +136 -0
  81. data/lib/hecks/projections/site/regions.rb +44 -0
  82. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  83. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  84. data/lib/hecks/projections/site/table/checks.rb +124 -0
  85. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  86. data/lib/hecks/projections/site/table.rb +98 -0
  87. data/lib/hecks/projections.rb +2 -0
  88. data/lib/hecks/projector/cli_projector.rb +122 -104
  89. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  90. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  91. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  92. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  93. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  94. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  95. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  96. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  97. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  98. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  99. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  100. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  101. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  102. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  103. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  104. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  105. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  106. data/lib/hecks/runtime/registry/verification.rb +15 -29
  107. data/lib/hecks/runtime/registry.rb +14 -20
  108. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  109. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  110. data/lib/hecks/rust_build/domain_name.rb +26 -0
  111. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  112. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  113. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  114. data/lib/hecks/rust_build/project_rust.rb +57 -24
  115. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  116. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  117. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  118. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  119. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  120. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  121. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  122. data/lib/hecks/storehouse.rb +37 -7
  123. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  124. data/lib/hecks/three_zero/forms.yml +88 -88
  125. data/lib/hecks/tools/ci_gates.rb +1 -1
  126. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  127. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  128. data/lib/hecks/tools/sequence_script.rb +2 -2
  129. data/lib/hecks/tools/site_routes.rb +152 -0
  130. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  131. data/lib/hecks/tools.rb +1 -0
  132. data/lib/hecks/version.rb +1 -1
  133. data/rust/build/src/cargo_sync.rs +1 -1
  134. data/rust/build/src/pipeline.rs +23 -16
  135. data/rust/build/src/resolve.rs +39 -6
  136. data/rust/host/HECKS_RELEASE +1 -1
  137. data/rust/host/src/api.rs +1 -1
  138. data/rust/host/src/dispatch.rs +87 -7
  139. data/rust/host/src/needs.rs +111 -0
  140. data/rust/parser/src/keywords.rs +7 -6
  141. data/rust/parser/src/main.rs +4 -3
  142. data/rust/parser/src/parse/chapter.rs +1 -1
  143. data/rust/parser/src/parse/hecksagon.rs +25 -10
  144. data/rust/project/mutations.rb +4 -30
  145. data/rust/project/naming.rb +8 -28
  146. data/rust/project/write_if_changed.rb +4 -80
  147. data/rust/project_rust_pipeline.rb +16 -12
  148. data/rust/src/kernel/cli.rs +2 -0
  149. data/rust/src/kernel/needs.rs +74 -1
  150. metadata +37 -1
@@ -1,7 +1,7 @@
1
1
  # The Deploy chapter: what `deployed_to(...)` may declare, and the commands that generate, check and
2
2
  # compare a deploy recipe, project the OIDC manifests and provision a tenant.
3
3
  #
4
- # `hecks deploy project` and `hecks deploy provision` boot it directly; the Hecks domain attaches it, so
4
+ # `hecks deploy recipe.project` and `hecks deploy tenant.provision` boot it directly; the Hecks domain attaches it, so
5
5
  # each command below is also `hecks deploy <verb>`. The targets validate the `deployed_to` shape
6
6
  # that `world.bluebook`'s untyped `Setting` map leaves open. Each run below records that it was
7
7
  # requested before its DeployToolchain port is asked, and what came of it after. No command
@@ -10,7 +10,7 @@ Hecks.bluebook "Deploy" do
10
10
  vision "Generate, check and compare deploy recipes, project OIDC manifests and provision tenants."
11
11
 
12
12
  aggregate "LambdaTarget" do
13
- description "One domain's validated AWS Lambda deploy target — what deployed_to(\"AwsLambda\") must resolve to before hecks deploy project generates anything from it."
13
+ description "One domain's validated AWS Lambda deploy target — what deployed_to(\"AwsLambda\") must resolve to before hecks deploy recipe.project generates anything from it."
14
14
 
15
15
  identified_by :domain
16
16
 
@@ -38,14 +38,14 @@ Hecks.bluebook "Deploy" do
38
38
  # (Serverless v2);
39
39
  # only the emitted CloudFormation resource differs, never the runtime adapter. "Shared" borrows
40
40
  # another domain's instance under its own Postgres schema and needs an `owner` setting, which
41
- # `hecks deploy project` checks, since the owning domain is a deploy-time fact.
41
+ # `hecks deploy recipe.project` checks, since the owning domain is a deploy-time fact.
42
42
  value_object "Database" do
43
43
  attribute :value, String, one_of: ["Postgres", "Aurora", "Shared"]
44
44
  end
45
45
 
46
46
  # "None" (default) or "Rust": the domain's own Lambda (rust/host) also serves a public web UI
47
47
  # in-process (rust/host/src/web.rs). "Ruby" is not a member: a Ruby WebFunction comes from a
48
- # `lambda_handler.rb` file, and `hecks deploy project` refuses `web "Rust"` alongside one.
48
+ # `lambda_handler.rb` file, and `hecks deploy recipe.project` refuses `web "Rust"` alongside one.
49
49
  value_object "Web" do
50
50
  attribute :value, String, one_of: ["None", "Rust"]
51
51
  end
@@ -87,7 +87,7 @@ Hecks.bluebook "Deploy" do
87
87
 
88
88
  # All required, no `default:`: command-level defaults apply only at the JSON/Rust-codegen
89
89
  # boundary, not Ruby's direct dispatch (interpreting.rb `normalize_args`). Defaults are
90
- # resolved by `hecks deploy project` before it dispatches.
90
+ # resolved by `hecks deploy recipe.project` before it dispatches.
91
91
  command "Declare" do
92
92
  goal "Validate one domain's AwsLambda deploy target before anything is generated from it"
93
93
 
@@ -122,7 +122,7 @@ Hecks.bluebook "Deploy" do
122
122
  # A sibling of `LambdaTarget` with its own value objects, since value objects are not
123
123
  # shared across aggregates.
124
124
  aggregate "FargateTarget" do
125
- description "One domain's validated AWS Fargate deploy target — what deployed_to(\"AwsFargate\") must resolve to before hecks deploy project generates anything from it."
125
+ description "One domain's validated AWS Fargate deploy target — what deployed_to(\"AwsFargate\") must resolve to before hecks deploy recipe.project generates anything from it."
126
126
 
127
127
  identified_by :domain
128
128
 
@@ -177,7 +177,7 @@ Hecks.bluebook "Deploy" do
177
177
  end
178
178
 
179
179
  # All seven required for the same reason as `LambdaTarget.Declare`; this validates a
180
- # target already resolved by `hecks deploy project`'s defaults.
180
+ # target already resolved by `hecks deploy recipe.project`'s defaults.
181
181
  command "Declare" do
182
182
  goal "Validate one domain's AwsFargate deploy target before anything is generated from it"
183
183
 
@@ -201,10 +201,83 @@ Hecks.bluebook "Deploy" do
201
201
  end
202
202
  end
203
203
 
204
- # The validated shape of `hecks deploy provision`, which does the generating and provisioning.
204
+ aggregate "BoxTarget" do
205
+ description "One domain's validated single-box deploy target — what deployed_to(\"AwsBox\") must resolve to before hecks deploy project generates a box and an RDS instance from it"
206
+
207
+ identified_by :domain
208
+
209
+ attribute :domain, DomainName
210
+ attribute :region, Region
211
+ attribute :instance_type, InstanceType
212
+ attribute :volume_gb, VolumeGb
213
+ attribute :database_class, DatabaseClass
214
+ attribute :storage_gb, StorageGb
215
+
216
+ value_object "DomainName" do
217
+ attribute :value, String
218
+ invariant("a domain is named") { !value.to_s.empty? }
219
+ end
220
+
221
+ value_object "Region" do
222
+ attribute :value, String
223
+ invariant("a region is named") { !value.to_s.empty? }
224
+ end
225
+
226
+ # An EC2 instance type such as t4g.medium. The family and size are checked for shape only;
227
+ # EC2 refuses an unknown type at stack creation.
228
+ value_object "InstanceType" do
229
+ attribute :value, String
230
+ invariant("an instance type is family.size, such as t4g.medium") { value.to_s.match?(/\A[a-z][a-z0-9-]*\.[a-z0-9]+\z/) }
231
+ end
232
+
233
+ # The box's root disk, which holds images, the compose files and container logs; the
234
+ # database is on RDS.
235
+ value_object "VolumeGb" do
236
+ attribute :value, Integer
237
+ invariant("a volume is at least 8 GB") { value >= 8 }
238
+ invariant("a volume is at most 16384 GB") { value <= 16384 }
239
+ end
240
+
241
+ # An RDS instance class such as db.t4g.small.
242
+ value_object "DatabaseClass" do
243
+ attribute :value, String
244
+ invariant("a database class is db.family.size, such as db.t4g.small") { value.to_s.match?(/\Adb\.[a-z][a-z0-9-]*\.[a-z0-9]+\z/) }
245
+ end
246
+
247
+ # RDS storage grows with the journal; 20 GB is RDS's gp3 floor.
248
+ value_object "StorageGb" do
249
+ attribute :value, Integer
250
+ invariant("storage is at least 20 GB") { value >= 20 }
251
+ invariant("storage is at most 65536 GB") { value <= 65536 }
252
+ end
253
+
254
+ # All six required for the same reason as `FargateTarget.Declare`; this validates a target
255
+ # already resolved by `hecks deploy project`'s defaults.
256
+ command "Declare" do
257
+ goal "Validate one domain's AwsBox deploy target before anything is generated from it"
258
+
259
+ attribute :domain, DomainName
260
+ attribute :region, Region
261
+ attribute :instance_type, InstanceType
262
+ attribute :volume_gb, VolumeGb
263
+ attribute :database_class, DatabaseClass
264
+ attribute :storage_gb, StorageGb
265
+
266
+ sets :domain
267
+ sets :region
268
+ sets :instance_type
269
+ sets :volume_gb
270
+ sets :database_class
271
+ sets :storage_gb
272
+
273
+ emits "BoxTargetDeclared"
274
+ end
275
+ end
276
+
277
+ # The validated shape of `hecks deploy tenant.provision`, which does the generating and provisioning.
205
278
  # Not a saga: each of its steps is idempotent and local, so there is nothing to resume.
206
279
  aggregate "Tenant" do
207
- description "One tenant's validated identity — which realm and Postgres schema `hecks deploy provision` provisions before booting a domain directory under Runtime.boot's own `environment:` overlay for it."
280
+ description "One tenant's validated identity — which realm and Postgres schema `hecks deploy tenant.provision` provisions before booting a domain directory under Runtime.boot's own `environment:` overlay for it."
208
281
 
209
282
  identified_by :slug
210
283
 
@@ -268,7 +341,7 @@ Hecks.bluebook "Deploy" do
268
341
  transition "Abandon" => "refused", from: "requested"
269
342
  end
270
343
 
271
- # `hecks deploy provision <directory> slug=s domain= realm= schema= database= [adapter=]`.
344
+ # `hecks deploy tenant.provision <directory> slug=s domain= realm= schema= database= [adapter=]`.
272
345
  # The only command that creates a tenant: its slug, schema and realm are validated by the value
273
346
  # objects' patterns and invariants, then the overlay is written through the TenantProvisioning
274
347
  # port; the adapter answers PostgresEra when `adapter` is left out. `dry_run?` on this verb
@@ -424,7 +497,7 @@ Hecks.bluebook "Deploy" do
424
497
  transition "Fault" => "faulted", from: "requested"
425
498
  end
426
499
 
427
- # `hecks deploy project <domain> [tenant=] [schema=] [out=] [environment=]`. A schema override
500
+ # `hecks deploy recipe.project <domain> [tenant=] [schema=] [out=] [environment=]`. A schema override
428
501
  # needs its tenant; the generator says so, and a domain with no `deployed_to` target, or one
429
502
  # whose settings conflict, is faulted with the generator's own sentence.
430
503
  command "Project" do
@@ -630,7 +703,7 @@ Hecks.bluebook "Deploy" do
630
703
  transition "Refuse" => "refused", from: "requested"
631
704
  end
632
705
 
633
- # `hecks deploy diff <before> after=<template> [--json] [--strict]`. `--strict` also counts
706
+ # `hecks deploy template_comparison.diff <before> after=<template> [--json] [--strict]`. `--strict` also counts
634
707
  # cosmetic differences (comments, key order); `--json` writes the report as JSON.
635
708
  command "Diff" do
636
709
  goal "Compare two CloudFormation templates and report what differs by logical id"
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "audience": "Deploy",
3
3
  "scopes": [
4
+ {
5
+ "scope": "deploy:box_target.declare",
6
+ "verb": "Deploy::BoxTarget.Declare",
7
+ "role": null
8
+ },
4
9
  {
5
10
  "scope": "deploy:fargate_target.declare",
6
11
  "verb": "Deploy::FargateTarget.Declare",
@@ -4,7 +4,8 @@ require_relative "../naming"
4
4
  module Hecks
5
5
  module Doc
6
6
  # The DSL reference, projected from the language's own Syntax chapter so
7
- # its tables can never drift from the live builders. Regenerate with `hecks project_reference`.
7
+ # its tables can never drift from the live builders. Regenerate with
8
+ # `hecks language_run.project_reference`.
8
9
  module Reference
9
10
  GENERATED_END = "<!-- generated:end -->".freeze
10
11
  TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
@@ -84,7 +85,7 @@ module Hecks
84
85
 
85
86
  *The tables on this page are generated from the language's own
86
87
  aggregate-local syntax tables (`lib/hecks/language/**/*.bluebook`)
87
- by `hecks project_reference` — do not edit inside the markers. The prose
88
+ by `hecks language_run.project_reference` — do not edit inside the markers. The prose
88
89
  between them is hand-written and survives regeneration.*
89
90
  #{GENERATED_END}
90
91
  #{"\n#{preamble}\n" unless preamble.empty?}
@@ -172,7 +173,7 @@ module Hecks
172
173
  # The DSL reference
173
174
 
174
175
  One page per context — the place in a file where a word may be
175
- typed. Generated by `hecks project_reference` from the Syntax chapter;
176
+ typed. Generated by `hecks language_run.project_reference` from the Syntax chapter;
176
177
  the prose between the generated markers is hand-written and
177
178
  survives regeneration.
178
179
 
@@ -11,17 +11,17 @@ module Hecks
11
11
 
12
12
  module_function
13
13
 
14
- # Builds a verb's nested, typed argument Hash from its `name=value` words.
14
+ # Builds a command's nested, typed argument Hash from its `name=value` words.
15
15
  #
16
16
  # arguments(spec, ["reference.value=A-1", "sequence.value=99"])
17
17
  # # => { reference: { value: "A-1" }, sequence: { value: 99 } }
18
18
  #
19
- # @param spec [Hash{Symbol => Object}] one verb's entry from `Projector::CliProjector`
20
- # @param pairs [Array<String>] the words after the verb: `name=value` pairs, where a path may
21
- # be the short form of a single-field value object (`reference` for `reference.value`);
22
- # `--name` for a Boolean; and at most one bare word, which fills the verb's first argument
19
+ # @param spec [Hash{Symbol => Object}] one command's entry from `Projector::CliProjector`
20
+ # @param pairs [Array<String>] the words after the command: `name=value` pairs, where a path
21
+ # may be the short form of a single-field value object (`reference` for `reference.value`);
22
+ # `--name` for a Boolean; and at most one bare word, which fills the first argument
23
23
  # @return [Hash{Symbol => Object}] the arguments nested by path, leaves cast to type
24
- # @raise [Runtime::NotFound] if a path, flag or bare word does not fit the verb
24
+ # @raise [Runtime::NotFound] if a path, flag or bare word does not fit the command
25
25
  # @raise [Runtime::TypeMismatch] if a value does not parse as its Integer or Float
26
26
  def arguments(spec, pairs)
27
27
  # Extra accepted arguments stay out of help, which teaches only to=....
@@ -44,7 +44,7 @@ module Hecks
44
44
 
45
45
  # Rewrites the short forms into `name=value`: `--name` for a Boolean (a following `yes`,
46
46
  # `no`, `true`, `false`, `on`, `off`, `1` or `0` is that flag's value, not an argument),
47
- # `--name=value` as `name=value`, and one bare word as the verb's first argument (`to` for
47
+ # `--name=value` as `name=value`, and one bare word as the command's first argument (`to` for
48
48
  # a command on an existing aggregate, the first attribute for one that creates).
49
49
  def normalize(spec, words, options)
50
50
  queue = words.dup
@@ -84,20 +84,20 @@ module Hecks
84
84
  path = options.key?(name) ? name : expand(name, options)
85
85
  return path if options.dig(path, :type) == "Boolean"
86
86
 
87
- raise Runtime::NotFound, "--#{name} is a flag, but this verb has no Boolean argument #{name.inspect}"
87
+ raise Runtime::NotFound, "--#{name} is a flag, but this command has no Boolean argument #{name.inspect}"
88
88
  end
89
89
 
90
- # The one bare word, as a pair for the verb's first argument the launcher does not mint.
90
+ # The one bare word, as a pair for the command's first argument the launcher does not mint.
91
91
  def positional(word, spec)
92
92
  first = spec[:arguments].find { |argument| !argument[:minted] }
93
93
  return "#{first[:path]}=#{word}" if first
94
94
 
95
95
  if spec[:arguments].any?
96
- raise Runtime::NotFound, "#{word.inspect} is not name=value; this verb's only argument is its run key, " \
96
+ raise Runtime::NotFound, "#{word.inspect} is not name=value; this command's only argument is its run key, " \
97
97
  "which is minted when omitted (run=<key> names one)"
98
98
  end
99
99
 
100
- raise Runtime::NotFound, "#{word.inspect} is not name=value, and this verb takes no arguments"
100
+ raise Runtime::NotFound, "#{word.inspect} is not name=value, and this command takes no arguments"
101
101
  end
102
102
 
103
103
  # Words the refusal for more than one unnamed argument.
@@ -187,9 +187,9 @@ module Hecks
187
187
  hash
188
188
  end
189
189
 
190
- # Words the refusal for an argument the verb does not take, listing what it does.
190
+ # Words the refusal for an argument the command does not take, listing what it does.
191
191
  def unknown(path, known)
192
- "no argument #{path.inspect} — this verb takes #{known.sort.join(', ')}"
192
+ "no argument #{path.inspect} — this command takes #{known.sort.join(', ')}"
193
193
  end
194
194
  end
195
195
  end
@@ -13,6 +13,8 @@ module Hecks
13
13
  module CliRunner
14
14
  # The class an adapter raises when the tool it wraps refuses.
15
15
  TOOL_REFUSAL = "Hecks::Adapters::ConsoleCapture::Failure".freeze
16
+ # The words that open the query namespace; `ask` is the older spelling.
17
+ QUERY_WORDS = %w[query ask].freeze
16
18
 
17
19
  module_function
18
20
 
@@ -20,7 +22,7 @@ module Hecks
20
22
  # The booted domain's own chapter is projected, or an attached one its first word names.
21
23
  #
22
24
  # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
23
- # @param argv [Array<String>] an optional `ask`, the verb or question name, then
25
+ # @param argv [Array<String>] an optional `query` (or `ask`), the command or query name, then
24
26
  # `path=value` pairs; `--help`, `-h`, `help` or nothing answers usage
25
27
  # @param program [String] how the caller was invoked, echoed in usage and hints
26
28
  # @return [Array(String, Integer)] the text and the status: 0 answered, 1 refused or misused
@@ -71,7 +73,7 @@ module Hecks
71
73
  cursor = nil
72
74
  loop do
73
75
  ask = cursor ? args.except(:from_now).merge(since: { value: cursor }) : args
74
- rows = runtime.query(spec[:verb], **ask)
76
+ rows = runtime.query(spec[:command], **ask)
75
77
  row = rows.first || {}
76
78
  Array(row[:events]).each { |event| out.puts(JSON.generate(entry_line(JsonDoor.materialize(event)))) }
77
79
  out.flush
@@ -93,23 +95,23 @@ module Hecks
93
95
  # Answers a command line that asks only for usage, from the projection alone.
94
96
  #
95
97
  # Needs a registry and no bound adapter, so a `Runtime::Loader::Described` serves: the
96
- # help text, the no-argument usage, a verb's `--help` and an unknown verb's hint all come
98
+ # help text, the no-argument usage, a command's `--help` and an unknown one's hint all come
97
99
  # out byte-identical to `call`'s.
98
100
  #
99
101
  # @param runtime [#registry] a booted domain, or what `Hecks.describe` answers
100
102
  # @param argv [Array<String>] as for `call`
101
103
  # @param program [String] how the caller was invoked
102
104
  # @return [Array(String, Integer), nil] the text and status, or nil when the line would
103
- # run a verb or question and so needs a booted domain
105
+ # run a command or question and so needs a booted domain
104
106
  def usage(runtime:, argv:, program: "hecks run")
105
107
  resolve(runtime, argv, program)[:answer]
106
108
  end
107
109
 
108
110
  # Parses a command line against the projection: either the answer it gives without
109
- # running anything, or the verb it would run.
111
+ # running anything, or the command it would run.
110
112
  #
111
113
  # @return [Hash] `{answer: [text, status]}`, or `{spec:, name:, rest:, asking:, program:,
112
- # bluebook:, launcher:}` for a verb to dispatch
114
+ # bluebook:, launcher:}` for a command to dispatch
113
115
  def resolve(runtime, argv, program)
114
116
  bluebook, argv, program = chapter_for(runtime, argv, program)
115
117
  launcher = LauncherOptions.settings(runtime, bluebook.name)
@@ -119,18 +121,11 @@ module Hecks
119
121
  name = argv.first
120
122
  return { answer: [cli[:usage], 0] } if name.nil? || %w[--help -h help].include?(name)
121
123
 
122
- # `ask` gives questions their own namespace; a chapter may declare a command and query
123
- # of one name.
124
- asking = name == "ask"
125
- argv = argv[1..] if asking
126
- name = argv.first
124
+ asking, name, argv = entry_word(cli, argv)
127
125
  return { answer: [cli[:usage], 1] } if name.nil?
128
126
 
129
- # A question answers to its bare name too; `ask` is needed only when a command shares it.
130
- asking ||= !cli[:names][:command].key?(name) && cli[:names][:question].key?(name)
131
-
132
- # The alias map lets `create_pizza` and `order.create_pizza` reach the same verb.
133
- pool = asking ? cli[:questions] : cli[:verbs]
127
+ # The alias map lets `create_pizza` and `order.create_pizza` reach the same command.
128
+ pool = asking ? cli[:questions] : cli[:commands]
134
129
  key = cli[:names][asking ? :question : :command][name]
135
130
  spec = pool[key]
136
131
  return { answer: [unknown(cli, name, asking, program), 1] } unless spec
@@ -138,7 +133,7 @@ module Hecks
138
133
  rest = argv[1..]
139
134
  if rest.include?("--help")
140
135
  help = Projector.call(:cli, bluebook: bluebook,
141
- options: options.merge(verb: name, ask: asking))[:usage]
136
+ options: options.merge(command: name, ask: asking))[:usage]
142
137
  return { answer: [help, 0] }
143
138
  end
144
139
 
@@ -146,6 +141,27 @@ module Hecks
146
141
  launcher: launcher }
147
142
  end
148
143
 
144
+ # Reads the entry words of a line: whether it asks a query, the bare name, and the words
145
+ # from the name on.
146
+ #
147
+ # `query` gives queries their own namespace (`ask` is its older spelling); a chapter may
148
+ # declare a command and a query of one name. A trailing `!` says the word is a command and
149
+ # is not part of the name. A query answers to its bare name too; `query` is needed only when
150
+ # a command shares it.
151
+ #
152
+ # @return [Array(Boolean, String, Array<String>)] `asking`, `name` (nil when absent), `argv`
153
+ def entry_word(cli, argv)
154
+ asking = QUERY_WORDS.include?(argv.first)
155
+ argv = argv[1..] if asking
156
+ name = argv.first
157
+ return [asking, nil, argv] if name.nil?
158
+
159
+ bang = name.end_with?("!")
160
+ name = name.chomp("!") if bang
161
+ asking ||= !bang && !cli[:names][:command].key?(name) && cli[:names][:question].key?(name)
162
+ [asking, name, argv]
163
+ end
164
+
149
165
  # The chapter a command line speaks to: the booted domain's own, or, when the first word
150
166
  # names a chapter its hecksagon attaches or uses as a framework member (`deploy`,
151
167
  # `governance`), that chapter, with the word dropped and added to the program name.
@@ -158,11 +174,11 @@ module Hecks
158
174
  [runtime.registry.bluebook(target), argv[1..], "#{program} #{argv.first}"]
159
175
  end
160
176
 
161
- # Parses one resolved verb's arguments, runs it as a query or command, and turns the
177
+ # Parses one resolved command's arguments, runs it as a query or command, and turns the
162
178
  # outcome, or the domain's refusal, into `[json_or_message, status]`. With `--wait`, a
163
179
  # question whose report names a gap (`LauncherOptions.gap_reported?`) answers status 1.
164
180
  #
165
- # @param bluebook [Bluebook::Chapter] the chapter the verb belongs to
181
+ # @param bluebook [Bluebook::Chapter] the chapter the command belongs to
166
182
  # @param launcher [Hash, nil] the chapter's `launcher` world setting; nil when not opted in
167
183
  def dispatch(runtime, spec, name, rest, program, asking, bluebook: nil, launcher: nil)
168
184
  rest, wait = LauncherOptions.take_wait(spec, rest) if launcher
@@ -171,10 +187,10 @@ module Hecks
171
187
  return answer_query(runtime, spec, args, wait) if spec[:kind] == :query
172
188
 
173
189
  args, minted = LauncherOptions.run_key(runtime, spec, args, launcher)
174
- # Answers with only this verb's outcome; a full store dump is every record there is.
190
+ # Answers with only this command's outcome; a full store dump is every record there is.
175
191
  request = CommandRequest.normalize(args, receiver: spec[:receiver],
176
192
  legacy_receiver: spec[:legacy_receiver])
177
- handle = runtime.dispatch_flat(spec[:verb], request)
193
+ handle = runtime.dispatch_flat(spec[:command], request)
178
194
  extra = refused_answer(handle)
179
195
  extra = { run: minted }.merge(extra) if minted
180
196
  return settled(runtime, spec, handle, bluebook, launcher, extra) if wait
@@ -185,7 +201,7 @@ module Hecks
185
201
  events: handle.events.map(&:name) }.merge(extra)), 0]
186
202
  rescue Runtime::NotFound, Runtime::TypeMismatch => e
187
203
  # A bad argument and a missing record both need the same next step: read the help.
188
- ["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
204
+ ["#{e.message}\n\n #{program} #{'query ' if asking}#{name} --help", 1]
189
205
  rescue *Runtime::DOMAIN_REFUSALS => e
190
206
  # The refusal is the chapter's own sentence, verbatim.
191
207
  [e.message, 1]
@@ -202,7 +218,7 @@ module Hecks
202
218
  # @param wait [Boolean, nil] whether `--wait` was given: a report naming a gap then fails
203
219
  # @return [Array(String, Integer)] the answer and the status
204
220
  def answer_query(runtime, spec, args, wait)
205
- rows = runtime.query(spec[:verb], **args)
221
+ rows = runtime.query(spec[:command], **args)
206
222
  text = text_answer(spec, rows)
207
223
  [text || JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }),
208
224
  wait && LauncherOptions.gap_reported?(text) ? 1 : 0]
@@ -256,7 +272,7 @@ module Hecks
256
272
 
257
273
  # The aggregate a top-level command belongs to; nil for an entity command or a port.
258
274
  def aggregate_of(bluebook, spec)
259
- head = spec[:verb].split("::", 2).last
275
+ head = spec[:command].split("::", 2).last
260
276
  return if head.count(".") != 1
261
277
 
262
278
  bluebook.aggregates.find { |aggregate| aggregate.hecks_name == head.split(".").first }
@@ -317,19 +333,23 @@ module Hecks
317
333
  end }
318
334
  end
319
335
 
320
- # Words the answer for an unknown verb or question, suggesting up to five near names.
336
+ # Words the answer for an unknown command or question, suggesting up to five near names.
321
337
  # Ranked by shared prefix, which survives a dropped letter where a substring match would not.
322
338
  def unknown(cli, name, asking, program)
323
339
  # Both spellings are candidates: `order.create_piza` shares no prefix with `create_pizza`.
324
340
  pool = cli[:names][asking ? :question : :command].keys
325
- near = pool.map { |candidate| [shared_prefix(candidate, name), candidate] }
326
- .select { |shared, _| shared >= [name.length / 2, 3].max }
327
- .sort_by { |shared, candidate| [-shared, candidate] }
328
- .map(&:last)
329
-
330
- lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
341
+ # A bare name is the commonest slip now that the aggregate is required: its homes first.
342
+ homes = (pool + cli[:names][asking ? :command : :question].keys)
343
+ .select { |candidate| candidate.split(".").last == name }.uniq
344
+ similar = pool.map { |candidate| [shared_prefix(candidate, name), candidate] }
345
+ .select { |shared, _| shared >= [name.length / 2, 3].max }
346
+ .sort_by { |shared, candidate| [-shared, candidate] }
347
+ .map(&:last)
348
+ near = homes + (similar - homes)
349
+
350
+ lines = ["no such #{asking ? 'query' : 'command'}: #{name}"]
331
351
  lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
332
- lines += ["", " #{program}#{' ask' if asking} for the full list"]
352
+ lines += ["", " #{program}#{' query' if asking} for the full list"]
333
353
  lines.join("\n")
334
354
  end
335
355
 
@@ -64,13 +64,13 @@ module Hecks
64
64
  $stderr = shown if hold
65
65
  end
66
66
 
67
- # Takes `--wait` out of a verb's words, unless the verb declares a `wait` argument of its own.
67
+ # Takes `--wait` out of a command's words, unless it declares a `wait` argument of its own.
68
68
  #
69
69
  # `--wait=false` (or `no`, `0`, `off`) is a `--wait` that was switched off; a bare `--wait`
70
70
  # may be followed by its Boolean word.
71
71
  #
72
- # @param spec [Hash] the verb's projected spec
73
- # @param words [Array<String>] the words after the verb
72
+ # @param spec [Hash] the command's projected spec
73
+ # @param words [Array<String>] the words after the command
74
74
  # @return [Array(Array<String>, Boolean)] the remaining words, and whether `--wait` was given
75
75
  # @raise [Runtime::TypeMismatch] if `--wait=` carries something that is not a Boolean word
76
76
  def take_wait(spec, words)
@@ -96,7 +96,7 @@ module Hecks
96
96
  def streams?(launcher, spec)
97
97
  return false unless launcher && spec[:kind] == :query
98
98
 
99
- Array(launcher[:streams]).map(&:to_s).include?(spec[:verb].to_s.split(/[.:]+/).last)
99
+ Array(launcher[:streams]).map(&:to_s).include?(spec[:command].to_s.split(/[.:]+/).last)
100
100
  end
101
101
 
102
102
  # Takes `--stream` out of a question's words.
@@ -137,7 +137,7 @@ module Hecks
137
137
  # rather than sent on without one.
138
138
  #
139
139
  # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
140
- # @param spec [Hash] the verb's projected spec
140
+ # @param spec [Hash] the command's projected spec
141
141
  # @param args [Hash] the parsed arguments
142
142
  # @param settings [Hash, nil] the chapter's `launcher` setting
143
143
  # @return [Array(Hash, String)] the arguments, and the key minted (nil when none was)
@@ -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 reader
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
- Hecks.boot(Storehouse.confine!(scope.admit_domain!(domain), "domain"), install_doors: false)
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
- runtime = boot(args["domain"])
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
- send_response(id, { tools: TOOLS.select { |tool| scope.permits_tool?(tool[:name]) } })
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"