hecks 3.0.5 → 3.1.1

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 (170) 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 +32 -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/domain_writer.rb +39 -0
  15. data/lib/hecks/cli/interview_agent.rb +59 -0
  16. data/lib/hecks/cli/interview_draft.rb +246 -0
  17. data/lib/hecks/cli/interview_run.rb +87 -0
  18. data/lib/hecks/cli/interview_session.rb +299 -0
  19. data/lib/hecks/cli/model_check.rb +1 -1
  20. data/lib/hecks/cli/project_cli.rb +51 -29
  21. data/lib/hecks/cli/reference.rb +2 -1
  22. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  23. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  24. data/lib/hecks/cli/spec_example.rb +1 -1
  25. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  26. data/lib/hecks/corpus.rb +12 -8
  27. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  28. data/lib/hecks/deploy/oidc.json +5 -0
  29. data/lib/hecks/doc/reference.rb +4 -3
  30. data/lib/hecks/doors/cli_door.rb +13 -13
  31. data/lib/hecks/doors/cli_runner.rb +53 -33
  32. data/lib/hecks/doors/launcher_options.rb +5 -5
  33. data/lib/hecks/doors/mcp_door.rb +40 -18
  34. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  35. data/lib/hecks/doors/usage_cache.rb +123 -0
  36. data/lib/hecks/doors.rb +1 -1
  37. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  38. data/lib/hecks/forms/banking_presentation.rb +1 -1
  39. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  40. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  41. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  42. data/lib/hecks/framework.rb +1 -1
  43. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  44. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  45. data/lib/hecks/fuzzing/differential.rb +4 -2
  46. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  47. data/lib/hecks/fuzzing/form_census.rb +2 -2
  48. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  49. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  50. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  51. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  52. data/lib/hecks/gate/stages.yml +5 -5
  53. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  54. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  55. data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
  56. data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
  57. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  58. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  59. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  60. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  61. data/lib/hecks/hecks/adapters/terminal.rb +15 -0
  62. data/lib/hecks/hecks/codebase.bluebook +4 -3
  63. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  64. data/lib/hecks/hecks/custodian.bluebook +81 -3
  65. data/lib/hecks/hecks/hecks.bluebook +18 -0
  66. data/lib/hecks/hecks/hecks.hecksagon +16 -4
  67. data/lib/hecks/hecks/hecks.world +7 -6
  68. data/lib/hecks/hecks/site.behaviors +17 -0
  69. data/lib/hecks/hecks/site.world +10 -0
  70. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  71. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  72. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  73. data/lib/hecks/projections/deploy/box/settings.rb +324 -0
  74. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
  75. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
  76. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  77. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
  78. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
  79. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  80. data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
  81. data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
  82. data/lib/hecks/projections/deploy/box.rb +553 -0
  83. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  84. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  85. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  86. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  87. data/lib/hecks/projections/diagrams.rb +2 -2
  88. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  89. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  90. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  91. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  92. data/lib/hecks/projections/site/edge.rb +136 -0
  93. data/lib/hecks/projections/site/regions.rb +44 -0
  94. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  95. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  96. data/lib/hecks/projections/site/table/checks.rb +124 -0
  97. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  98. data/lib/hecks/projections/site/table.rb +98 -0
  99. data/lib/hecks/projections.rb +2 -0
  100. data/lib/hecks/projector/cli_projector.rb +153 -111
  101. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  102. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  103. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  104. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  105. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  106. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  107. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  108. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  109. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  110. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  111. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  112. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  113. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  114. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  115. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  116. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  117. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  118. data/lib/hecks/runtime/loader.rb +31 -8
  119. data/lib/hecks/runtime/registry/verification.rb +15 -29
  120. data/lib/hecks/runtime/registry.rb +14 -20
  121. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  122. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  123. data/lib/hecks/rust_build/domain_name.rb +26 -0
  124. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  125. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  126. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  127. data/lib/hecks/rust_build/project_rust.rb +57 -24
  128. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  129. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  130. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  131. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  132. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  133. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  134. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  135. data/lib/hecks/storehouse.rb +37 -7
  136. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  137. data/lib/hecks/three_zero/forms.yml +88 -88
  138. data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
  139. data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
  140. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
  141. data/lib/hecks/tools/ci_gates.rb +1 -1
  142. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  143. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  144. data/lib/hecks/tools/sequence_script.rb +2 -2
  145. data/lib/hecks/tools/site_routes.rb +152 -0
  146. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  147. data/lib/hecks/tools.rb +1 -0
  148. data/lib/hecks/version.rb +1 -1
  149. data/lib/hecks.rb +1 -0
  150. data/rust/build/src/cargo_sync.rs +1 -1
  151. data/rust/build/src/pipeline.rs +23 -16
  152. data/rust/build/src/resolve.rs +39 -6
  153. data/rust/codegen/src/json_codec.rs +3 -0
  154. data/rust/codegen/src/naming.rs +4 -0
  155. data/rust/codegen/src/types.rs +45 -0
  156. data/rust/host/HECKS_RELEASE +1 -1
  157. data/rust/host/src/api.rs +1 -1
  158. data/rust/host/src/dispatch.rs +87 -7
  159. data/rust/host/src/needs.rs +111 -0
  160. data/rust/parser/src/keywords.rs +7 -6
  161. data/rust/parser/src/main.rs +4 -3
  162. data/rust/parser/src/parse/chapter.rs +1 -1
  163. data/rust/parser/src/parse/hecksagon.rs +25 -10
  164. data/rust/project/mutations.rb +4 -30
  165. data/rust/project/naming.rb +8 -28
  166. data/rust/project/write_if_changed.rb +4 -80
  167. data/rust/project_rust_pipeline.rb +16 -12
  168. data/rust/src/kernel/cli.rs +2 -0
  169. data/rust/src/kernel/needs.rs +74 -1
  170. metadata +51 -2
@@ -1,9 +1,9 @@
1
1
  Hecks.hecksagon "Hecks" do
2
- uses_framework "Governance"
2
+ attaches "Governance"
3
3
 
4
4
  # The chapters the gem carries: the language declared in itself (Paging comes with Bluebook),
5
- # Expression, Tenancy, Deploy and QualityControl (which brings its own ports and adapters).
6
- %w[Bluebook Hecksagon World Adapter Port Translation Expression Tenancy Deploy QualityControl]
5
+ # Expression, Tenancy, Deploy, Site, Tickets and QualityControl (the last three bring their own ports).
6
+ %w[Bluebook Hecksagon World Adapter Port Translation Expression Tenancy Deploy Site Tickets QualityControl]
7
7
  .each { |chapter| attaches chapter }
8
8
  # Persistence is not bound here: hecks.world's `default_adapter` binds every aggregate, so
9
9
  # environments/memory.world can swap the whole domain without a double bind.
@@ -95,6 +95,12 @@ Hecks.hecksagon "Hecks" do
95
95
  answers "DoorAnswered"
96
96
  refuses "DoorRefused"
97
97
  end
98
+
99
+ asks "Scaffold", to: Door do
100
+ attribute :run, DoorKey
101
+ answers "DoorAnswered"
102
+ refuses "DoorRefused"
103
+ end
98
104
  end
99
105
 
100
106
  Hecks::Door.port "Terminal" do
@@ -103,6 +109,12 @@ Hecks.hecksagon "Hecks" do
103
109
  answers "DoorAnswered"
104
110
  refuses "DoorRefused"
105
111
  end
112
+
113
+ asks "Converse", to: Door do
114
+ attribute :run, DoorKey
115
+ answers "DoorAnswered"
116
+ refuses "DoorRefused"
117
+ end
106
118
  end
107
119
 
108
120
  Hecks::Build.port "RustToolchain" do
@@ -394,7 +406,7 @@ Hecks.hecksagon "Hecks" do
394
406
  end
395
407
 
396
408
  # The one seam where Deploy's "Tenant" (a validated request) becomes Tenancy's "Tenant" (a fact),
397
- # as tenancy.hecksagon declares it for the chapters booted on their own: `hecks deploy provision`
409
+ # as tenancy.hecksagon declares it for the chapters booted on their own: `hecks deploy tenant.provision`
398
410
  # registers the tenant it provisioned. It sits here, after `attaches` has loaded both chapters.
399
411
  Hecks.hecksagon "Tenancy" do
400
412
  translates "RegisterProvisionedTenant" do
@@ -15,7 +15,7 @@
15
15
  # Postgres (a `postgres://` URL, or a bare database name); otherwise a local database named for
16
16
  # the domain. It connects as the ordinary role `hecks`, never a superuser: the era write-fence
17
17
  # (row-level security) is bypassed by one, so boot refuses it. Create the role once with
18
- # `hecks quality_control create_ledger_role hecks` and the database with `createdb -O hecks hecks`.
18
+ # `hecks quality_control sweep.create_ledger_role hecks` and the database with `createdb -O hecks hecks`.
19
19
  #
20
20
  # PostgresEra versions the storage shape. Once a database holds an era, a later change to this
21
21
  # domain's shape needs a translation edge, kept in `translations/` beside the bluebook; an
@@ -26,9 +26,9 @@
26
26
  # `failure_states` are the lifecycle states `--wait` reports as exit 1 (every failure state the
27
27
  # chapter's lifecycles declare), `names` gives a command the launcher name clients type,
28
28
  # `executable` is where `hecks project_cli` writes this chapter's launcher (the gem's
29
- # `exe/hecks`), `streams` lists the questions `--stream` may tail (`Follow`), `memory_verbs` names
30
- # the verbs that launcher runs on the Memory environment unless `HECKS_ENVIRONMENT` is set (so
31
- # `hecks console` needs no database), and `legacy` names the verbs it hands to `Hecks::CLI` in the
29
+ # `exe/hecks`), `streams` lists the questions `--stream` may tail (`Follow`), `memory_commands` names
30
+ # the commands that launcher runs on the Memory environment unless `HECKS_ENVIRONMENT` is set (so
31
+ # `hecks console` needs no database), and `legacy` names the commands it hands to `Hecks::CLI` in the
32
32
  # positional forms ADR 0066 shipped.
33
33
  Hecks.world "Hecks" do
34
34
  realm "Hecks"
@@ -37,10 +37,11 @@ Hecks.world "Hecks" do
37
37
  launcher "Launcher", run_keys: true,
38
38
  failure_states: %w[flagged failed drifted unreachable refused faulted halted stopped
39
39
  abandoned red needs_fix],
40
- names: { "mcp" => "serve_mcp", "console" => "open_console" },
40
+ names: { "mcp" => "door.serve_mcp", "console" => "operation.open_console", "init" => "door.init",
41
+ "interview" => "door.interview" },
41
42
  streams: %w[Follow],
42
43
  executable: "exe/hecks",
43
- memory_verbs: %w[console],
44
+ memory_commands: %w[console init interview],
44
45
  legacy: %w[run docs narrate ir stores model_check smoke_test
45
46
  project_diagrams project_cli mcp]
46
47
  end
@@ -0,0 +1,17 @@
1
+ Hecks.behaviors "Site" do
2
+ vision "A projection of a site's route table is recorded as requested before anything outside is asked."
3
+
4
+ loads "hecks.bluebook", "custodian.bluebook", "codebase.bluebook", "context_map.hecksagon", "hecks.hecksagon", "site.world", "environments/memory.world"
5
+
6
+ test "ProjectSite keeps a project that has no bluebook as a faulted projection" do
7
+ tests "ProjectSite", on: "SiteProjection"
8
+ input run: { value: "routes" }, domain: { value: "/tmp/hecks-behaviors-absent/project" }
9
+ expect emits: %w[SiteProjectionRequested SiteRefused SiteFaulted]
10
+ end
11
+
12
+ test "ProjectSite refuses a run that is not named" do
13
+ tests "ProjectSite", on: "SiteProjection"
14
+ input run: { value: "" }, domain: { value: "/tmp/hecks-behaviors-absent/project" }
15
+ expect refused: "must match"
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ # The Site chapter's launcher when the Hecks domain attaches it: `hecks site <verb>` reads the
2
+ # world named for the chapter, as `deploy.world` does for the Deploy chapter. Its store is not set
3
+ # here: hecks.world's `default_adapter` fills the gap, so the memory overlay swaps it with the rest.
4
+ #
5
+ # `run_keys` mints the `run` key of a projection, and `--wait` reports a faulted projection (a
6
+ # routes.ts out of date under `--check`, or a route table the projector refused) as exit 1.
7
+ Hecks.world "Site" do
8
+ launcher "Launcher", run_keys: true,
9
+ failure_states: %w[faulted refused halted stopped abandoned red needs_fix]
10
+ end
@@ -2,7 +2,7 @@
2
2
  # Memory: this chapter is read, never written to a store.
3
3
  Hecks.hecksagon "Bluebook" do
4
4
  # `role "Language"` is real access control only once Governance can check it (S8).
5
- uses_framework "Governance"
5
+ attaches "Governance"
6
6
  persisted_by "Memory"
7
7
  end
8
8
 
@@ -5,9 +5,7 @@ Hecks.bluebook "Hecksagon" do
5
5
  identified_by HecksagonName, as: :domain
6
6
 
7
7
  attribute :subscriptions, list_of(Subscription)
8
- attribute :framework_members, list_of(FrameworkMember)
9
- attribute :vendored_bluebooks, list_of(VendoredBluebook)
10
- attribute :attached_chapters, list_of(AttachedChapter)
8
+ attribute :attachments, list_of(Attachment)
11
9
 
12
10
  value_object "HecksagonName" do
13
11
  attribute :value, String
@@ -19,22 +17,20 @@ Hecks.bluebook "Hecksagon" do
19
17
  invariant("a subscription names an event") { !value.to_s.empty? }
20
18
  end
21
19
 
22
- # A `framework/bluebook/` member this hecksagon attaches, such as Governance or Identity.
23
- value_object "FrameworkMember" do
24
- attribute :value, String
25
- invariant("a framework member is named") { !value.to_s.empty? }
26
- end
27
-
28
- # A separate bluebook package vendored into the consuming project, not shipped in hecks's lib/.
29
- value_object "VendoredBluebook" do
30
- attribute :value, String
31
- invariant("a vendored bluebook is named") { !value.to_s.empty? }
20
+ # A chapter this hecksagon attaches: `value` is its name, `source` is `gem` for a chapter the
21
+ # gem carries (a framework member such as Governance, or a language chapter such as Deploy)
22
+ # or `vendor` for a package vendored into the consuming project, not shipped in hecks's lib/.
23
+ value_object "Attachment" do
24
+ attribute :value, String
25
+ attribute :source, String, default: "gem"
26
+ invariant("an attachment is named") { !value.to_s.empty? }
27
+ invariant("an attachment comes from the gem or from vendor") { source == "gem" || source == "vendor" }
32
28
  end
33
29
 
34
- # A chapter the gem carries (the language, Expression, Tenancy, Deploy) this hecksagon attaches.
35
- value_object "AttachedChapter" do
30
+ # The name an attachment is asked for by, before its source is known.
31
+ value_object "ChapterName" do
36
32
  attribute :value, String
37
- invariant("an attached chapter is named") { !value.to_s.empty? }
33
+ invariant("a chapter is named") { !value.to_s.empty? }
38
34
  end
39
35
 
40
36
  command "Declare" do
@@ -59,35 +55,14 @@ Hecks.bluebook "Hecksagon" do
59
55
  emits "SubscriptionAdded"
60
56
  end
61
57
 
62
- command "UsesFramework" do
63
- role "Deployment"
64
- goal "Attach a framework/bluebook/ member to this hecksagon"
65
-
66
- attribute :member, FrameworkMember
67
-
68
- sets :framework_members, append: { value: :member }
69
-
70
- emits "FrameworkMemberAttached"
71
- end
72
-
73
- command "UsesEmbryonautBluebook" do
74
- role "Deployment"
75
- goal "Attach a vendored, external bluebook to this hecksagon"
76
-
77
- attribute :package, VendoredBluebook
78
-
79
- sets :vendored_bluebooks, append: { value: :package }
80
-
81
- emits "VendoredBluebookAttached"
82
- end
83
-
84
58
  command "Attaches" do
85
59
  role "Deployment"
86
- goal "Attach a chapter the gem carries to this hecksagon, by name"
60
+ goal "Attach a chapter to this hecksagon, from the gem or from vendor"
87
61
 
88
- attribute :chapter, AttachedChapter
62
+ attribute :chapter, ChapterName
63
+ attribute :source, String, default: "gem"
89
64
 
90
- sets :attached_chapters, append: { value: :chapter }
65
+ sets :attachments, append: { value: :chapter, source: :source }
91
66
 
92
67
  emits "ChapterAttached"
93
68
  end
@@ -107,9 +82,10 @@ Hecks.bluebook "Hecksagon" do
107
82
 
108
83
  member word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: ""
109
84
  member word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions"
110
- member word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "framework_members"
111
- member word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "vendored_bluebooks"
112
- member word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attached_chapters"
85
+ member word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments"
86
+ # Deprecated spellings of `attaches`; they warn and are removed in 3.2.0.
87
+ member word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
88
+ member word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
113
89
  member word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", calls: "port_impl"
114
90
  # A cross-domain reaction wired here rather than in the bluebook; reuses the Policy context.
115
91
  # No `calls:` — HecksagonBuilder#translates is an ordinary method, found before `word_gate`.
@@ -138,9 +114,10 @@ Hecks.bluebook "Hecksagon" do
138
114
 
139
115
  member keyword: "hecksagon", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "domain"
140
116
  member keyword: "subscribe", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "subscriptions"
141
- member keyword: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "framework_members"
142
- member keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "vendored_bluebooks"
143
- member keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attached_chapters"
117
+ member keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments"
118
+ member keyword: "attaches", context: "Hecksagon", at: "", named: "from", kind: "symbol", required: "false", fills: "source"
119
+ member keyword: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments"
120
+ member keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments"
144
121
  member keyword: "port", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name"
145
122
  member keyword: "translates", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name"
146
123
 
@@ -16,7 +16,7 @@ module Hecks
16
16
 
17
17
  # Runs both halves of the era gate over every bluebook in a registry.
18
18
  #
19
- # Looks up each bluebook's own source separately: `uses_framework` can share a boot between
19
+ # Looks up each bluebook's own source separately: `attaches` can share a boot between
20
20
  # the domain's own bluebook and a differently-sourced one, so one file read once and reused
21
21
  # for every bluebook would attribute the wrong text.
22
22
  #
@@ -84,7 +84,7 @@ module Hecks
84
84
  # Reads the source text one bluebook was declared in, wherever that source lives.
85
85
  #
86
86
  # Matched by declared name, not position: a domain directory using
87
- # `uses_framework` may hold more than one file, and a framework
87
+ # `attaches` may hold more than one file, and a framework
88
88
  # member or vendored bluebook is excluded from the single-file
89
89
  # fallback so its own real source is read, not a sibling's.
90
90
  #
@@ -150,11 +150,11 @@ module Hecks
150
150
  #
151
151
  # @param registry [Runtime::Registry] the registry whose hecksagons are searched
152
152
  # @param bluebook_name [String] the bluebook's declared Pascal-case name
153
- # @return [String, nil] the package name as written in `uses_embryonaut_bluebook`; nil
153
+ # @return [String, nil] the package name as written in `attaches ... from: :vendor`; nil
154
154
  # when no hecksagon vendored a package whose name Pascal-cases to `bluebook_name`
155
155
  def vendored_bluebook_name_for(registry, bluebook_name)
156
156
  registry.hecksagons.each_value do |hecksagon|
157
- match = hecksagon.vendored_bluebooks.find { |name| Naming.pascal(name) == bluebook_name }
157
+ match = hecksagon.vendored_packages.find { |name| Naming.pascal(name) == bluebook_name }
158
158
  return match if match
159
159
  end
160
160
  nil
@@ -0,0 +1,324 @@
1
+ module Hecks
2
+ module Projections
3
+ module Deploy
4
+ module Box
5
+ # Resolves a world's `deployed_to("AwsBox")` settings into the checked plan the box
6
+ # generator renders from.
7
+ #
8
+ # Every string that reaches a template or a script is matched against a conservative
9
+ # pattern first, so a world file cannot splice shell, YAML or Caddy syntax into the output.
10
+ # A setting the world leaves out resolves to the default the golden stack was built with.
11
+ module Settings
12
+ NAME = /\A[a-z][a-z0-9-]{0,40}\z/
13
+ REPOSITORY = %r{\A[a-z0-9][a-z0-9._/-]{1,100}\z}
14
+ URL_PATH = %r{\A/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*\z}
15
+ SECRET_NAME = %r{\A[A-Za-z0-9/_+=.@-]{1,256}\*?\z}
16
+ ENV_KEY = /\A[A-Za-z_][A-Za-z0-9_]*\z/
17
+ ENV_VALUE = /\A[^\x00-\x1f\x7f]*\z/
18
+ HEADER = /\A[A-Za-z][A-Za-z0-9-]{0,63}\z/
19
+ DB_NAME = /\A[a-zA-Z][a-zA-Z0-9]{0,62}\z/
20
+ ENGINE = /\A\d{2}(\.\d{1,2})?\z/
21
+ PREFIX = /\A[a-z][a-z0-9-]{0,20}\z/
22
+ IMAGE = %r{\A[a-z0-9][a-z0-9._/:@-]{1,200}\z}
23
+ TASKDEF = /\A[a-zA-Z0-9_-]{1,255}\z/
24
+ SCHEMA = /\A[a-z][a-z0-9_]{0,62}\z/
25
+ MIGRATION_SHAPE = "migration: a hash needs `schemas`, a list of the schema names to copy".freeze
26
+ S3_BUCKET = /\A[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]\z/
27
+ S3_SHAPE = "s3_access: a list of `{ bucket: \"name\", write: true }` hashes (write is optional)".freeze
28
+ FROM_TASKDEF = %i[env secrets repository].freeze
29
+ # Default images, each a version tag plus the digest of its multi-architecture index,
30
+ # so a rebuilt box pulls the same bytes.
31
+ TUNNEL_IMAGE = "cloudflare/cloudflared:2026.9.3" \
32
+ "@sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c".freeze
33
+ PROXY_IMAGE = "public.ecr.aws/docker/library/caddy:2.8" \
34
+ "@sha256:226d1f059b75399fe19182893c7184591c07b97afc8dfcf44eeb80c9a77a530f".freeze
35
+ TUNNEL_SHAPE = "tunnel: a hash needs `to` (the container it forwards to) and `token_secret` " \
36
+ "(the secret holding the tunnel token)".freeze
37
+ ORIGIN_PAIR = "origin_header and origin_secret go together: the header a CDN sends, " \
38
+ "and the secret that holds its value".freeze
39
+
40
+ # One container the box runs: its image repository, its port and its settings.
41
+ #
42
+ # @!attribute [r] name [String] the compose service name
43
+ # @!attribute [r] repository [String] the ECR repository holding its images
44
+ # @!attribute [r] port [Integer] the port it listens on, on the box's own network
45
+ # @!attribute [r] env [Hash{String => String}] plain environment variables
46
+ # @!attribute [r] secrets [Hash{String => String}] environment variable => secret name,
47
+ # resolved on the box at deploy time and never written into a template
48
+ Container = Struct.new(:name, :repository, :port, :env, :secrets, keyword_init: true)
49
+
50
+ # A set of URL paths one container serves.
51
+ #
52
+ # @!attribute [r] container [String] the container's name
53
+ # @!attribute [r] paths [Array<String>] Caddy path patterns, such as "/cms/*"
54
+ Route = Struct.new(:container, :paths, keyword_init: true)
55
+
56
+ # A Cloudflare tunnel the box runs as a service, pointed at one container.
57
+ #
58
+ # @!attribute [r] container [String] the container the tunnel forwards to
59
+ # @!attribute [r] port [Integer] that container's port
60
+ # @!attribute [r] token_secret [String] the secret holding the tunnel token
61
+ # @!attribute [r] image [String] the cloudflared image
62
+ Tunnel = Struct.new(:container, :port, :token_secret, :image, keyword_init: true)
63
+
64
+ # The data a project moves from its old database into the new RDS instance.
65
+ #
66
+ # @!attribute [r] schemas [Array<String>] the schemas to copy
67
+ # @!attribute [r] database [String] the database holding them on the RDS instance
68
+ # @!attribute [r] source_database [String] the database holding them on the old server
69
+ Migration = Struct.new(:schemas, :database, :source_database, keyword_init: true)
70
+
71
+ # An S3 bucket the box's role may read, and in production write.
72
+ #
73
+ # @!attribute [r] name [String] the bucket's name
74
+ # @!attribute [r] write [Boolean] whether a production box may also write and delete
75
+ Bucket = Struct.new(:name, :write, keyword_init: true)
76
+
77
+ # Everything the generator reads, checked.
78
+ Plan = Struct.new(
79
+ :infra_name, :stack_prefix, :instance_type, :volume_gb, :swap_gb, :database_class,
80
+ :storage_gb, :backup_days, :snapshots_keep, :database_name, :engine_version,
81
+ :containers, :routes, :default_container, :origin_header, :origin_secret,
82
+ :secret_prefixes, :tunnel, :tunnel_service, :proxy_image, :task_definition,
83
+ :migration, :s3_buckets, keyword_init: true
84
+ ) do
85
+ # @return [String] the CloudFormation stack that holds the database
86
+ def rds_stack = "#{stack_prefix}-#{infra_name}-rds"
87
+
88
+ # @return [String] the CloudFormation stack that holds the box
89
+ def box_stack = "#{stack_prefix}-#{infra_name}-box"
90
+
91
+ # @return [Container] the container that answers every path no route claims
92
+ def default
93
+ containers.find { |c| c.name == default_container }
94
+ end
95
+ end
96
+
97
+ module_function
98
+
99
+ # @param deploy_settings [Hash{Symbol => Object}] the world's `AwsBox` settings
100
+ # @param target [Object] the validated `BoxTarget` (its instance type, volume, database
101
+ # class and storage, already checked by `Declare`)
102
+ # @param infra_name [String] the stack name the world or the domain gives
103
+ # @return [Plan] the resolved plan
104
+ # @raise [ArgumentError] when a setting is missing, malformed or inconsistent
105
+ def resolve(deploy_settings:, target:, infra_name:)
106
+ s = deploy_settings
107
+ check(:stack_name, infra_name, NAME)
108
+ database_name = read_database_name(s, infra_name)
109
+ listed = s.fetch(:containers) { raise ArgumentError, missing_containers }
110
+ task_definition = read_task_definition(s[:task_definition], listed)
111
+ containers = read_containers(listed, infra_name)
112
+ header, secret = read_origin(s)
113
+ tunnel, tunnel_service = read_tunnel(s.fetch(:tunnel, false), containers)
114
+
115
+ Plan.new(
116
+ infra_name: infra_name, stack_prefix: check(:stack_prefix, s.fetch(:stack_prefix, "hecks"), PREFIX),
117
+ **declared_sizes(target), **read_sizes(s), database_name: database_name,
118
+ engine_version: check(:engine_version, s.fetch(:engine_version, "16").to_s, ENGINE),
119
+ containers: containers, routes: read_routes(s.fetch(:routes, []), containers),
120
+ default_container: read_default(s[:default_container], containers), origin_header: header,
121
+ origin_secret: secret, secret_prefixes: read_prefixes(s, infra_name),
122
+ tunnel: tunnel, tunnel_service: tunnel_service,
123
+ proxy_image: check(:proxy_image, s.fetch(:proxy_image, PROXY_IMAGE), IMAGE),
124
+ task_definition: task_definition, migration: read_migration(s[:migration], database_name),
125
+ s3_buckets: read_s3_access(s.fetch(:s3_access, []))
126
+ )
127
+ end
128
+
129
+ # @param target [Object] the declared `BoxTarget`
130
+ # @return [Hash{Symbol => Object}] the sizes `Declare` has already validated
131
+ def declared_sizes(target)
132
+ %i[instance_type volume_gb database_class storage_gb].to_h { |key| [key, target.state[key].value] }
133
+ end
134
+
135
+ # @param settings [Hash{Symbol => Object}] the world's settings
136
+ # @return [Hash{Symbol => Integer}] swap, backup retention and snapshot retention, bounded
137
+ def read_sizes(settings)
138
+ {
139
+ swap_gb: integer(:swap_gb, settings.fetch(:swap_gb, 2), 0, 64),
140
+ backup_days: integer(:backup_days, settings.fetch(:backup_days, 7), 1, 35),
141
+ snapshots_keep: integer(:snapshots_keep, settings.fetch(:snapshots_keep, 7), 1, 1000)
142
+ }
143
+ end
144
+
145
+ # @param value [Hash{Symbol => Object}, nil] the world's `migration` setting
146
+ # @param database_name [String] the RDS database, which the schemas move into by default
147
+ # @return [Migration, nil] the data to move, or nil when the world declares none
148
+ # @raise [ArgumentError] when the setting is not a hash with a non-empty list of schemas
149
+ def read_migration(value, database_name)
150
+ return nil if value.nil?
151
+ raise ArgumentError, MIGRATION_SHAPE unless value.is_a?(Hash)
152
+
153
+ listed = value.fetch(:schemas) { raise ArgumentError, MIGRATION_SHAPE }
154
+ raise ArgumentError, MIGRATION_SHAPE unless listed.is_a?(Array) && !listed.empty?
155
+
156
+ database = check(:migration_database, value.fetch(:database, database_name), DB_NAME)
157
+ Migration.new(schemas: listed.map { |name| check(:migration_schemas, name, SCHEMA) }.uniq, database: database,
158
+ source_database: check(:migration_source_database, value.fetch(:source_database, database), DB_NAME))
159
+ end
160
+
161
+ # @param list [Array<Hash>] the world's `s3_access` setting
162
+ # @return [Array<Bucket>] the buckets, each readable and optionally writable in production
163
+ # @raise [ArgumentError] when the setting is not a list of bucket hashes
164
+ def read_s3_access(list)
165
+ raise ArgumentError, S3_SHAPE unless list.is_a?(Array)
166
+
167
+ list.map do |spec|
168
+ raise ArgumentError, S3_SHAPE unless spec.is_a?(Hash)
169
+
170
+ name = check(:s3_bucket, spec.fetch(:bucket) { raise ArgumentError, S3_SHAPE }, S3_BUCKET)
171
+ Bucket.new(name: name, write: boolean(:s3_write, spec.fetch(:write, false)))
172
+ end.uniq(&:name)
173
+ end
174
+
175
+ def read_database_name(settings, infra_name)
176
+ check(:database_name, settings.fetch(:database_name, infra_name.gsub(/[^a-zA-Z0-9]/, "")), DB_NAME)
177
+ end
178
+
179
+ # With a task definition, the images, environment and secrets are read from it at deploy
180
+ # time, so a container that also sets them is ambiguous and refused.
181
+ #
182
+ # @param family [String, nil] the ECS task definition family the world names
183
+ # @param listed [Array<Hash>] the containers as the world wrote them
184
+ # @return [String, nil] the checked family, or nil when the world does not use one
185
+ # @raise [ArgumentError] when the family is malformed or a container sets what it supplies
186
+ def read_task_definition(family, listed)
187
+ return nil if family.nil?
188
+
189
+ check(:task_definition, family, TASKDEF)
190
+ Array(listed).each do |spec|
191
+ clash = spec.is_a?(Hash) ? FROM_TASKDEF & spec.keys : []
192
+ next if clash.empty?
193
+
194
+ raise ArgumentError, "containers: #{spec[:name]} sets #{clash.join(', ')}, which the task definition " \
195
+ "#{family} supplies; drop #{clash.size == 1 ? 'it' : 'them'} or drop task_definition"
196
+ end
197
+ family
198
+ end
199
+
200
+ def read_containers(list, infra_name)
201
+ raise ArgumentError, missing_containers unless list.is_a?(Array) && !list.empty?
202
+
203
+ containers = list.map { |c| read_container(c, infra_name) }
204
+ names = containers.map(&:name)
205
+ dup = names.find { |n| names.count(n) > 1 }
206
+ raise ArgumentError, "containers: two containers are named #{dup.inspect}" if dup
207
+
208
+ ports = containers.map(&:port)
209
+ clash = ports.find { |p| ports.count(p) > 1 }
210
+ raise ArgumentError, "containers: two containers listen on port #{clash}; they share the box's network" if clash
211
+
212
+ containers
213
+ end
214
+
215
+ def read_container(spec, infra_name)
216
+ raise ArgumentError, "containers: each container is a hash, got #{spec.inspect}" unless spec.is_a?(Hash)
217
+
218
+ name = check(:container_name, spec.fetch(:name) { raise ArgumentError, "containers: a container has no name" }, NAME)
219
+ Container.new(
220
+ name: name, repository: check(:repository, spec.fetch(:repository, "#{infra_name}-#{name}"), REPOSITORY),
221
+ port: integer(:port, spec.fetch(:port) { raise ArgumentError, "containers: #{name} has no port" }, 1, 65_535),
222
+ env: string_map(:env, spec.fetch(:env, {}), ENV_KEY, ENV_VALUE),
223
+ secrets: string_map(:secrets, spec.fetch(:secrets, {}), ENV_KEY, SECRET_NAME)
224
+ )
225
+ end
226
+
227
+ def read_routes(list, containers)
228
+ raise ArgumentError, "routes: expected a list" unless list.is_a?(Array)
229
+
230
+ list.map { |spec| read_route(spec, containers) }
231
+ end
232
+
233
+ def read_route(spec, containers)
234
+ container = spec.fetch(:container) { raise ArgumentError, "routes: a route has no container" }
235
+ name = check(:route_container, container, NAME)
236
+ known_container!(:routes, name, containers)
237
+ paths = Array(spec.fetch(:paths) { raise ArgumentError, "routes: the route for #{name} has no paths" })
238
+ raise ArgumentError, "routes: the route for #{name} has no paths" if paths.empty?
239
+
240
+ Route.new(container: name, paths: paths.map { |p| check(:path, p, URL_PATH) })
241
+ end
242
+
243
+ def read_default(name, containers)
244
+ return containers.first.name if name.nil? && containers.size == 1
245
+ raise ArgumentError, "default_container: name the container that answers every other path" if name.nil?
246
+
247
+ known_container!(:default_container, check(:default_container, name, NAME), containers)
248
+ end
249
+
250
+ def known_container!(key, name, containers)
251
+ return name if containers.any? { |c| c.name == name }
252
+
253
+ raise ArgumentError, "#{key}: #{name.inspect} is not a declared container"
254
+ end
255
+
256
+ def read_origin(settings)
257
+ header = settings[:origin_header]
258
+ secret = settings[:origin_secret]
259
+ return [nil, nil] if header.nil? && secret.nil?
260
+ raise ArgumentError, ORIGIN_PAIR if header.nil? || secret.nil?
261
+
262
+ [check(:origin_header, header, HEADER), check(:origin_secret, secret, SECRET_NAME)]
263
+ end
264
+
265
+ # `tunnel true` only opens the outbound port; a hash also runs cloudflared as a service.
266
+ #
267
+ # @param value [Boolean, Hash{Symbol => Object}] the world's `tunnel` setting
268
+ # @param containers [Array<Container>] the declared containers
269
+ # @return [Array(Boolean, Tunnel)] whether the egress opens, and the service if one runs
270
+ def read_tunnel(value, containers)
271
+ return [boolean(:tunnel, value), nil] unless value.is_a?(Hash)
272
+
273
+ to = value.fetch(:to) { raise ArgumentError, TUNNEL_SHAPE }
274
+ target = known_container!(:tunnel, check(:tunnel_to, to, NAME), containers)
275
+ token = check(:tunnel_token_secret, value.fetch(:token_secret) { raise ArgumentError, TUNNEL_SHAPE }, SECRET_NAME)
276
+ image = check(:tunnel_image, value.fetch(:image, TUNNEL_IMAGE), IMAGE)
277
+ port = containers.find { |c| c.name == target }.port
278
+ [true, Tunnel.new(container: target, port: port, token_secret: token, image: image)]
279
+ end
280
+
281
+ def read_prefixes(settings, infra_name)
282
+ list = Array(settings.fetch(:secret_prefixes, ["#{infra_name}/*"]))
283
+ list.map { |p| check(:secret_prefixes, p, SECRET_NAME) }
284
+ end
285
+
286
+ def string_map(key, value, key_pattern, value_pattern)
287
+ raise ArgumentError, "#{key}: expected a hash" unless value.is_a?(Hash)
288
+
289
+ value.to_h { |k, v| [check(key, k.to_s, key_pattern), check(key, v.to_s, value_pattern)] }
290
+ end
291
+
292
+ def check(key, value, pattern)
293
+ return value if value.is_a?(String) && value.match?(pattern)
294
+
295
+ raise ArgumentError, "#{key}: #{value.inspect} does not match #{pattern.inspect}"
296
+ end
297
+
298
+ def integer(key, value, low, high)
299
+ return value if value.is_a?(Integer) && value.between?(low, high)
300
+
301
+ raise ArgumentError, "#{key}: #{value.inspect} must be an integer from #{low} to #{high}"
302
+ end
303
+
304
+ def boolean(key, value)
305
+ return value if [true, false].include?(value)
306
+
307
+ raise ArgumentError, "#{key}: #{value.inspect} must be true or false"
308
+ end
309
+
310
+ def missing_containers
311
+ <<~MSG
312
+ deployed_to("AwsBox") needs at least one container. Add one, e.g.:
313
+
314
+ deployed_to("AwsBox") do
315
+ region "us-east-1"
316
+ containers [{ name: "web", port: 8080 }]
317
+ end
318
+ MSG
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end