hecks 2.8.0 → 2.9.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -4,46 +4,8 @@ require_relative "shared"
4
4
  module Hecks
5
5
  module Projections
6
6
  module Deploy
7
- # The AWS Lambda deploy target — docs/decisions/0018-rehydrate-replay-lambda-host.md.
8
- # An export (`Projector::Target#projects_as`'s own `needs_world: true`),
9
- # not an ordinary projection: it reads a domain's own
10
- # `deployed_to("AwsLambda")` `.world` settings, not only its
11
- # declaration, because a running system has to know how it is wired.
12
- #
13
- # `bin/project_deploy` finds and boots the domain's own chapter and
14
- # its `.world`/`.hecksagon` bindings, calls this through
15
- # `Projector.call(:aws_lambda, bluebook:, options:, world:)`, and
16
- # writes the returned tree — see that script for the CLI-facing parts
17
- # (ARGV parsing, `--tenant`/`--schema`, finding the `.world` file) this
18
- # target never needs to know about.
19
- #
20
- # ## What this generates
21
- #
22
- # The SAM template and build Makefile for `rust/host` (the
23
- # wasmtime+Postgres Lambda entry point) from a domain's own `.world`
24
- # file, the same way `bin/project_wasm` generates the `.wasm` artifact
25
- # from the domain's own `.bluebook` — no hand-authored deployment
26
- # config for any given domain, only a generator run against whatever it
27
- # declares:
28
- #
29
- # deployed_to("AwsLambda") { region "..."; memory 512; timeout 10 }
30
- #
31
- # is a verb in a domain's `.world` file, read through `Hecks.world`'s
32
- # own generic settings bag
33
- # (`lib/hecks/bluebook/dsl/world_builder.rb#method_missing`).
34
- #
35
- # Self-contained: the returned `template.yaml` owns its own private
36
- # VPC, subnets, and RDS Postgres instance, not just the Lambda — no
37
- # externally-supplied database, no secret anyone has to type (RDS's
38
- # own `ManageMasterUserPassword` plus a CloudFormation dynamic
39
- # reference compose `DATABASE_URL` at deploy time). `PackageType:
40
- # Zip`, `Runtime: provided.al2023` — no container, no Docker, no ECR.
41
- #
42
- # `Shared` (`lib/hecks/projections/deploy/shared.rb`) carries the parts
43
- # of this generator `Fargate` needs too — VPC/subnet/security-group
44
- # resources, RDS/Aurora, `bastion.yaml`, and the era-minting/
45
- # translation Make recipes — plain functions, not a registered target
46
- # of their own.
7
+ # The AWS Lambda deploy target (ADR 0018): renders `template.yaml`,
8
+ # `Makefile`, `samconfig.toml`, and `bastion.yaml` from a domain's `.world`.
47
9
  module Lambda
48
10
  extend Projector::Target
49
11
 
@@ -51,37 +13,14 @@ module Hecks
51
13
 
52
14
  module_function
53
15
 
54
- # Generates `template.yaml`, `Makefile`, `samconfig.toml`, and
55
- # (unless this domain borrows another domain's RDS instance)
56
- # `bastion.yaml` for one domain's `deployed_to("AwsLambda")` deploy
57
- # target.
16
+ # Renders `template.yaml`, `Makefile`, `samconfig.toml`, and (unless
17
+ # this domain shares another domain's RDS instance) `bastion.yaml`.
58
18
  #
59
- # `bluebook:` establishes admission — a real chapter, not merely a
60
- # directory — but generation itself reads from `options`: every
61
- # loaded chapter (`options[:cross_domain_registry]`), not only this
62
- # one, is what a cross-domain policy's own invoke grant needs.
63
- #
64
- # @param bluebook [Bluebook::Behaviour::Chapter] the domain's own booted chapter
65
- # @param options [Hash] generation options
66
- # @option options [Bluebook::World] :world the domain's `.world` settings,
67
- # required — `Projector.call`'s own `world:` keyword merges this in
68
- # @option options [String] :domain_dir the domain directory's path
69
- # @option options [String] :root the hecks project root, for `rust/host`,
70
- # `rust/dist`, and this project's own `Gemfile.lock`
71
- # @option options [String] :world_file the `.world` file's own path, quoted in
72
- # refusal messages
73
- # @option options [Runtime::Registry] :cross_domain_registry every chapter this
74
- # domain loads (its own, plus any framework attachments), for resolving
75
- # cross-domain policy invoke targets
76
- # @option options [Hash] :tenant `--tenant`/`--schema` overrides
77
- # (`{tenant: "acme", schema: "acme"}`), or omitted for an ordinary deploy
78
- # @return [Hash{String => String}] `"template.yaml"`, `"Makefile"`,
79
- # `"samconfig.toml"`, and — unless this domain declares `database "Shared"` —
80
- # `"bastion.yaml"`, each mapped to its rendered content
81
- # @raise [ArgumentError] if the domain's own deploy settings conflict (both
82
- # `web "Rust"` and a `lambda_handler.rb`, `database "Shared"` with no
83
- # `owner`, and the other refusals `deploy.bluebook`'s own
84
- # `LambdaTarget.Declare` and this generator raise)
19
+ # @param bluebook [Bluebook::Behaviour::Chapter] the domain's booted chapter
20
+ # @param options [Hash] :world, :domain_dir, :root, :world_file, and
21
+ # :cross_domain_registry are required; :tenant is optional
22
+ # @return [Hash{String => String}] generated file contents by filename
23
+ # @raise [ArgumentError] if `deployed_to("AwsLambda")` is invalid
85
24
  def call(bluebook:, options: {})
86
25
  world = options.fetch(:world)
87
26
  domain = options.fetch(:domain_dir)
@@ -95,19 +34,9 @@ module Hecks
95
34
 
96
35
  deploy_settings = world.for_verb("deployed_to")
97
36
 
98
- # **`PII` detection** — structural only, no live boot: `load_bluebooks`/a
99
- # bare `.hecksagon` `Kernel.load` populate `registry.pending_privacy_
100
- # markings` (`AggregateDoor#mark_sensitive`/`BindingProxy#mark_sensitive`,
101
- # lib/hecks/runtime/registry.rb) the same way `Runtime::Loader.boot`'s
102
- # own first phase does, without that method's later `run_boot_gates!`/
103
- # `dispatcher_for` steps — those need a live persistence adapter (a real
104
- # Postgres connection for a PostgresEra-bound domain), which a static
105
- # template generator must never require. A registry of its own, not
106
- # `cross_domain_registry` — that one boots this domain's bluebook too,
107
- # but through a different, hand-rolled port/adapter load (persistence
108
- # + extraction + memory + prism only) than the one `pending_privacy_
109
- # markings` was verified against; kept separate rather than assumed
110
- # equivalent.
37
+ # Structural-only load (no `run_boot_gates!`/live persistence adapter)
38
+ # populates `registry.pending_privacy_markings` for pii detection below
39
+ # without requiring a real Postgres connection to generate a template.
111
40
  pii_registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain))
112
41
  Hecks.with_registry(pii_registry) do
113
42
  bootstrap = Hecks::Ports::Loading.bootstrap
@@ -118,21 +47,12 @@ module Hecks
118
47
  Dir.glob(File.join(bluebook_dir, "*.hecksagon")).each { |file| Kernel.load(file) }
119
48
  end
120
49
 
121
- # Only `category: "pii"` counts — `phi` or any other vocabulary a
122
- # consuming domain's own `mark_sensitive` calls use stays a Governance/
123
- # redaction concern (Privacy::Marking's own mechanism) without also
124
- # provisioning CloudFront/WAF, which this generator has no way to know
125
- # is warranted for, say, health data under a different compliance
126
- # regime entirely. A domain wanting the same protection for a
127
- # non-"pii" category marks it "pii" too — the category string is
128
- # open-ended by design (privacy.bluebook's own header), not a closed
129
- # enum this generator could instead enumerate.
50
+ # Only `category: "pii"` triggers CloudFront/WAF provisioning; other
51
+ # marking categories (e.g. "phi") stay a Governance/redaction concern.
130
52
  pii_detected = pii_registry.pending_privacy_markings.any? { |marking| marking[:category].to_s == "pii" }
131
53
 
132
- # **The tenant override itself** — see this file's own header comment on
133
- # `--tenant`/`--schema` for the full reasoning. Applied here, before
134
- # `infra_name`/`hecks_schema` are computed below, so both read it the
135
- # same way they already read any other `deployed_to` setting.
54
+ # Applied before infra_name/hecks_schema below, so both read the
55
+ # override the same way they read any other `deployed_to` setting.
136
56
  if tenant_options[:tenant]
137
57
  base_stack_name = deploy_settings[:stack_name] || domain_name
138
58
  deploy_settings = deploy_settings.merge(
@@ -141,57 +61,20 @@ module Hecks
141
61
  )
142
62
  end
143
63
 
144
- # **Every AWS-facing name below** — stack, Lambda logical id (and so the
145
- # RDS logical id derived from it, `#{logical_id}Db`), S3 prefixes,
146
- # bastion/secret names — reads this, not `domain_name`, from here on.
147
- # Ordinarily they're the same string, and most domains never set
148
- # `stack_name` at all. They diverge on purpose for a domain whose
149
- # declared identity (`Hecks.bluebook`, hence `world.domain`, hence
150
- # `.world`'s own filename — `domain_name` above, needed just to find
151
- # that file) does not match its live AWS stack's own name: a
152
- # CloudFormation stack cannot be renamed in place, and this domain's
153
- # logical ids are already load-bearing on a real RDS instance other
154
- # domains borrow from in Shared mode (see `owner_stack_name` below) —
155
- # regenerating the template under a new stack/logical-id would not
156
- # rename anything, it would stand up a second, empty, disconnected
157
- # set of AWS resources next to the real one. `stack_name
158
- # "embryonaut"` in `deployed_to("AwsLambda")` pins the original name
159
- # forever, independent of any later `formerly_known_as`-style rename
160
- # to the domain's own declared identity.
64
+ # Every AWS-facing name (stack, logical ids, S3 prefixes, secrets)
65
+ # reads `infra_name`, not `domain_name`, so a `stack_name` override
66
+ # survives a later `formerly_known_as` rename of the declared identity
67
+ # without renaming (and disconnecting from) the live AWS stack.
161
68
  infra_name = deploy_settings[:stack_name] || domain_name
162
69
 
163
- # **The actual Postgres/RDS identifier** — every domain before this one
164
- # ever picked a single-word `stack_name` (or none at all, falling back
165
- # to a single-word directory `domain_name`), so `infra_name` itself was
166
- # already a valid database name and nothing ever needed a second
167
- # variable. `stack_name "quality-control-webhook"` (a real, live,
168
- # multi-word choice — the first one) broke that assumption for real:
169
- # RDS's own DBName/DatabaseName parameter refuses it outright
170
- # ("DBName must begin with a letter and contain only alphanumeric
171
- # characters" — confirmed live, a genuine CREATE_FAILED before this
172
- # existed), and `infra_name` unsanitized is also what every downstream
173
- # consumer (WebFunction/#{logical_id}'s own DB_NAME Environment
174
- # variable, every Makefile shell command that connects `-d
175
- # #{infra_name}`) reads as the database to actually connect to — so
176
- # every one of those has to agree on the same sanitized spelling, not
177
- # just the one CloudFormation property that happened to refuse loudly.
178
- # Stack names, logical ids, and S3 prefixes are unchanged (still read
179
- # `infra_name` directly) — CloudFormation/S3 both allow hyphens fine;
180
- # only the actual database identifier needed this at all.
70
+ # RDS's own DBName/DatabaseName parameter refuses non-alphanumeric
71
+ # characters; every downstream consumer (Environment vars, Makefile
72
+ # shell commands) has to read the same sanitized spelling.
181
73
  db_name = infra_name.gsub(/[^a-zA-Z0-9]/, "")
182
74
 
183
- # Validated, not hand-checked — dispatches into lib/hecks/deploy's
184
- # own LambdaTarget.Declare (the same given/invariant machinery every
185
- # other kind of bluebook mistake in this codebase is caught by),
186
- # replacing a `fetch(:region) { abort ... }` chain with
187
- # real, named, corpus-testable refusals: memory/timeout are actual
188
- # Integers here (never silently stringified) and checked against
189
- # Lambda's own real 128-10240 MB / 900s ceilings, not just "present or
190
- # not." Only the default values stay Ruby's own concern —
191
- # deploy.bluebook's own header explains why: a command-level `default:`
192
- # is honored only at the JSON/Rust-codegen boundary, never by Ruby's
193
- # own direct dispatch — so this validates a fully-resolved target,
194
- # after defaulting, not instead of it.
75
+ # Dispatches into LambdaTarget.Declare's given/invariant machinery for
76
+ # named, corpus-testable refusals (memory/timeout checked as real
77
+ # Integers against Lambda's 128-10240 MB / 900s ceilings).
195
78
  deploy_dispatcher = Hecks.boot(File.expand_path("../../deploy", __dir__))
196
79
  begin
197
80
  target = deploy_dispatcher.dispatch(
@@ -201,14 +84,7 @@ module Hecks
201
84
  region: { value: deploy_settings[:region] },
202
85
  memory: { value: deploy_settings.fetch(:memory, 512) },
203
86
  timeout: { value: deploy_settings.fetch(:timeout, 10) },
204
- # "Postgres" — Embryonaut's own live choice, and every domain
205
- # declared before this setting existed — unless a domain's own
206
- # deployed_to("AwsLambda") names "Aurora" explicitly.
207
87
  database: { value: deploy_settings.fetch(:database, "Postgres") },
208
- # "None" — every domain declared before this setting existed,
209
- # Embryonaut included (its own public web UI is the separate,
210
- # pre-existing lambda_handler.rb-file-presence mechanism, not this
211
- # attribute) — unless a domain names "Rust" explicitly.
212
88
  web: { value: deploy_settings.fetch(:web, "None") }
213
89
  }
214
90
  ).instance
@@ -216,68 +92,26 @@ module Hecks
216
92
  raise ArgumentError, "#{world_file}'s deployed_to(\"AwsLambda\") is invalid: #{e.message}"
217
93
  end
218
94
 
219
- # `dispatch "None"` / `secret_env "..."` — deploy-time wiring facts, not
220
- # business invariants `Deploy::LambdaTarget.Declare` itself needs to
221
- # hold, read straight off `deploy_settings` the same way `owner`/
222
- # `owner_stack`/`schema`/`stack_name` already are (this file's own
223
- # comment on `infra_name`, above, has the full reasoning for why those
224
- # stay Ruby-level rather than validated attributes).
225
- #
226
- # `dispatch "None"` decides whether the primary rust/host dispatch
227
- # Lambda (`#{logical_id}`, computed further below) gets generated at
228
- # all. Default (unset, every domain declared
229
- # before this setting existed) keeps generating the rust/host wasmtime
230
- # dispatch Lambda exactly as always — "None" is the new, opt-in case: no
231
- # rust/host dispatch Lambda for this domain at all, because a domain
232
- # with no `.bluebook`-shaped command surface of its own to dispatch
233
- # through (QualityControl's own real first user: a driving GitHub
234
- # webhook adapter, `lib/hecks/adapters/driving/github_webhook.rb`,
235
- # calling straight into `QualityControl::Clearance` through Ruby, never
236
- # through rust/host's wasmtime dispatch at all) has nothing for that
237
- # Lambda to ever serve. Reuses the existing `lambda_handler.rb`-file-
238
- # presence WebFunction mechanism wholesale (Runtime: ruby3.2, Function
239
- # URL AuthType `NONE`, container build + patch-pg-native for `pg`'s native
240
- # extension) rather than inventing a second Ruby-Lambda shape — the
241
- # WebFunction path already proved itself for real (Embryonaut's own live
242
- # stack) before this ever needed a second consumer; deploy.bluebook's
243
- # own generator, "one generator reads what a domain declares," extends
244
- # rather than forks.
95
+ # `dispatch "None"` skips generating a rust/host dispatch Lambda for a
96
+ # domain with no `.bluebook` command surface to dispatch through (e.g.
97
+ # QualityControl's GitHub webhook, dispatching straight into Ruby);
98
+ # such a domain's WebFunction becomes its only Lambda.
245
99
  dispatch_none = deploy_settings[:dispatch].to_s == "None"
246
100
 
247
- # `secret_env "GITHUB_WEBHOOK_SECRET"` — names the env var
248
- # WebFunction's own `lambda_handler.rb` should find the fetched secret's
249
- # real plaintext under, once it resolves `#{secret_env}_ARN` (below,
250
- # WebFunction's own Environment) via the AWS SDK the same cold-start
251
- # pattern DATABASE_URL/SESSION_SECRET/GOOGLE_CLIENT_ID already use
252
- # (WebFunction's own header comment: "fetch at runtime, over the SDK,
253
- # never let CloudFormation/Lambda configuration see it at all"). Wired
254
- # generically — any WebFunction-shaped domain declaring one gets its own
255
- # auto-generated, never-typed-or-seen SecretsManager secret + least-
256
- # privilege grant, not something specific to GitHub webhooks; this
257
- # domain's own `qa/lambda_handler.rb` is simply the first real consumer.
101
+ # Names the env var WebFunction's `lambda_handler.rb` finds the fetched
102
+ # secret's plaintext under, once it resolves `#{secret_env}_ARN`.
258
103
  webhook_secret_env = deploy_settings[:secret_env]
259
104
 
260
- # `handler_module "QaWebhookLambdaHandler"` — the same name
261
- # `lambda_handler.rb`'s own module actually defines, named here rather
262
- # than assumed, since `dispatch "None"` is a generic mechanism (any
263
- # future domain could adopt it, each with its own wrapper module name)
264
- # — "WebLambdaHandler" stays the fixed, historical default for every
265
- # domain that has never set this (Embryonaut's own, unchanged).
105
+ # The module name `lambda_handler.rb` actually defines; named here
106
+ # since `dispatch "None"` domains may each pick their own.
266
107
  webhook_handler_module = deploy_settings.fetch(:handler_module, "WebLambdaHandler")
267
108
  if dispatch_none && !deploy_settings.key?(:handler_module)
268
109
  raise ArgumentError, "#{world_file}'s deployed_to(\"AwsLambda\") declares dispatch \"None\" but no handler_module — add handler_module \"YourModuleName\" naming the module #{domain}/lambda_handler.rb defines."
269
110
  end
270
111
 
271
- # Every policy in every loaded chapter, not just the target domain's
272
- # own top-level list — `uses_framework` (Compliance's own header:
273
- # "genuinely supports either mode") could in principle attach a chapter
274
- # that itself declares a cross-domain policy, and a policy nested inside
275
- # `aggregate "X" do ... end` is exactly as real a cross-domain trigger as
276
- # one declared at the bluebook's own top level (docs/implemented/guides/policies-
277
- # and-process-managers.md: "A policy does not care where it was
278
- # written"). `to_h`'s own `policies` already flattens aggregate-scoped
279
- # and top-level policies into one list per bluebook — read the same way
280
- # `rust/project/reactions.rb`'s `emit_cross_domain_policy_table` does.
112
+ # Scans every loaded chapter, not just this domain's own — a
113
+ # `uses_framework`-attached chapter can itself declare a cross-domain
114
+ # policy, including one nested inside `aggregate "X" do ... end`.
281
115
  cross_domain_lambda_targets = cross_domain_registry.bluebooks.flat_map { |chapter_name, bluebook|
282
116
  bluebook.policies.select(&:target_domain).map(&:target_domain)
283
117
  }.uniq.sort
@@ -291,15 +125,8 @@ module Hecks
291
125
  shared = database == "Shared"
292
126
  rust_web = target.state[:web].value == "Rust"
293
127
 
294
- # **`WAFv2` for CloudFront needs us-east-1** — not a preference, a hard
295
- # AWS API constraint (`AWS::WAFv2::WebACL` with `Scope: CLOUDFRONT` is
296
- # refused by CloudFormation outside us-east-1, independent of which
297
- # region the distribution itself, being global, would otherwise
298
- # suggest). Refused here, before writing a single file, the same
299
- # discipline this generator already holds every other one of its own
300
- # refusals to — a generated template that would only fail at deploy
301
- # time, in a region a caller may not think to suspect, is a worse
302
- # failure mode than refusing now with the fix named.
128
+ # AWS::WAFv2::WebACL with Scope: CLOUDFRONT is refused by
129
+ # CloudFormation outside us-east-1; refused here before writing a file.
303
130
  if pii_detected && region != "us-east-1"
304
131
  raise ArgumentError, "#{world_file} marks a field \"pii\" but deployed_to(\"AwsLambda\") sets region " \
305
132
  "#{region.inspect} — a CloudFront-scoped WAFv2 WebACL can only be created in " \
@@ -307,25 +134,13 @@ module Hecks
307
134
  "genuinely holds none."
308
135
  end
309
136
 
310
- # Read straight off `deploy_settings` (the raw `WorldBuilder` bag), not
311
- # through `target` — these two are optional and pii-only, unlike every
312
- # `target.state[...]` field above, which `LambdaTarget` validates as
313
- # always-present for every `AwsLambda` deployment regardless of pii.
314
- # "none" (`RestrictionType: none`, `Locations: []`) is CloudFront's own
315
- # default shape for "no restriction configured" — declaring one later
316
- # is a `deployed_to` edit, not a template rewrite.
137
+ # Read off `deploy_settings` directly, not `target` — these two are
138
+ # optional and pii-only, unlike every always-validated `target.state`.
317
139
  geo_restriction_type = deploy_settings[:geo_restriction] || "none"
318
140
  geo_restriction_countries = deploy_settings[:geo_restriction_countries] || []
319
141
 
320
- # **The storehouse** — this domain provisions no RDS/VPC of its own at all;
321
- # it borrows another already-deployed domain's instance instead,
322
- # isolated by a native Postgres schema (this domain's own lowercase
323
- # name) rather than a separate database. `owner` is read the same way
324
- # `pg_version`/`google_oauth_present` below are — a Ruby-level fact off
325
- # `deploy_settings` directly, not one of `Deploy::LambdaTarget.Declare`'s
326
- # own validated attributes (deploy.bluebook's own comment on why: which
327
- # domain owns the shared instance is a deploy-time wiring fact, not a
328
- # business invariant).
142
+ # Shared mode provisions no RDS/VPC of its own; it borrows another
143
+ # already-deployed domain's instance, isolated by Postgres schema.
329
144
  if shared
330
145
  owner_domain_name = deploy_settings[:owner] or raise ArgumentError, <<~MSG
331
146
  #{world_file}'s deployed_to("AwsLambda") declares database "Shared" but no owner. Add one, e.g.:
@@ -338,182 +153,83 @@ module Hecks
338
153
 
339
154
  naming the already-deployed domain whose Postgres instance this one borrows.
340
155
  MSG
341
- # `hecks-<lowercase name>` / lowercase name-as-dbname — the same
342
- # two conventions `stack_name`/`DBName: #{infra_name}` below already
343
- # commit to for this domain, and the default for the owner too. Not
344
- # always the owner's real stack name, though — a domain generated
345
- # before this convention existed keeps whatever it was actually
346
- # deployed as (real, live example: Embryonaut's own stack is
347
- # "hecksagain-embryonaut", a legacy prefix, not "hecks-embryonaut" --
348
- # `deploy:`'s own owner-outputs lookup below would look for a stack
349
- # that has never existed and fail before this domain's own `sam
350
- # deploy` ever ran). `owner_stack "hecksagain-embryonaut"` is the
351
- # escape hatch, read the same deploy_settings-direct way `owner`
352
- # itself is (deploy.bluebook's own comment on why neither lives in
353
- # the validated LambdaTarget aggregate) — optional, defaults to the
354
- # ordinary convention for every domain that doesn't need it.
156
+ # `owner_stack` is the escape hatch for an owner whose live stack
157
+ # name predates the "hecks-<lowercase name>" convention below.
355
158
  owner_stack_name = deploy_settings[:owner_stack] || "hecks-#{owner_domain_name.downcase}"
356
159
  owner_db_name = owner_domain_name.downcase
357
160
  end
358
161
 
359
- # `HECKS_SCHEMA` — rust/host's own optional counterpart to Ruby's
360
- # `settings[:schema]` (postgres.rb's own `connect_for`). Automatic for a
361
- # Shared-mode domain (this domain's own lowercase name — matches the
362
- # schema the real migration creates for it, `CREATE SCHEMA
363
- # #{infra_name}`, and needs no separate declaration since Shared mode
364
- # already implies it). Optional and explicit otherwise
365
- # (`deployed_to("AwsLambda") { schema "embryonaut" }`) — for an owning
366
- # domain that later migrates itself onto a shared instance's own schema
367
- # too (Embryonaut, in the real migration this exists for), without
368
- # switching its own `database` setting away from where its real RDS
369
- # instance already lives.
162
+ # rust/host's optional counterpart to Ruby's `settings[:schema]`.
163
+ # Automatic (this domain's lowercase name) in Shared mode; otherwise
164
+ # explicit via `deployed_to("AwsLambda") { schema "..." }`.
370
165
  hecks_schema = shared ? infra_name : deploy_settings[:schema]
371
166
 
372
167
  logical_id = "#{infra_name.split(/[_-]/).map(&:capitalize).join}Function"
373
- # `stack_prefix` — the "hecks-" half of this domain's own stack name, a
374
- # setting only for a domain whose live stack predates the convention
375
- # (real, live example: Embryonaut's own stack, both its Lambda function
376
- # names, and its Google OAuth secret are all "hecksagain-embryonaut*",
377
- # generated by the pre-rename hecksagain fork). Same shape and
378
- # reasoning as `owner_stack` above — read `deploy_settings`-direct, not
379
- # validated by LambdaTarget, optional — but for this domain rather than
380
- # the owner it borrows from. Everything downstream (stack, FunctionName
381
- # for both Lambdas, the OAuth secret name and its IAM policy, the
382
- # bastion stack, samconfig's stack_name/s3_prefix) reads `stack_name`,
383
- # so one setting moves them all together; `infra_name` (logical ids,
384
- # deploy/<dir>, DB name) is untouched — that is `stack_name`'s job.
385
- # Defaults to "hecks", so every domain that doesn't set it regenerates
386
- # byte-identically.
168
+ # The "hecks-" half of this domain's own stack name; overridable for a
169
+ # live stack that predates the convention. Everything downstream
170
+ # (FunctionNames, OAuth secret, bastion stack, samconfig) reads
171
+ # `stack_name`; `infra_name` (logical ids, DB name) is untouched.
387
172
  stack_prefix = deploy_settings[:stack_prefix] || "hecks"
388
173
  stack_name = "#{stack_prefix}-#{infra_name}"
389
174
 
390
175
  db_id = "#{logical_id.sub(/Function\z/, '')}Db"
391
- # The Aurora shape splits the database in two (a DBCluster carrying
392
- # the managed password/endpoint, plus at least one DBInstance inside
393
- # it) — every `.Endpoint.Address`/`.MasterUserSecret.SecretArn`
394
- # reference below has to point at the cluster, not `db_id` itself,
395
- # when Aurora is chosen. The plain-RDS path is unaffected: `db_ref_id`
396
- # equals `db_id` exactly, so every one of those references renders
397
- # byte-identical to before this existed.
176
+ # Aurora splits the database into a DBCluster plus DBInstance(s), so
177
+ # every `.Endpoint.Address`/`.MasterUserSecret` reference below has to
178
+ # point at the cluster, not `db_id`, when Aurora is chosen.
398
179
  db_ref_id = aurora ? "#{db_id}Cluster" : db_id
399
180
 
400
- # The Aurora path uses a self-managed secret (`#{db_id}Secret`, below —
401
- # see its own comment for why: ManageMasterUserPassword's rotation
402
- # breaks a Lambda's static Environment), not the plain-RDS path's own
403
- # `db_ref_id.MasterUserSecret` attribute (which only exists for the
404
- # managed-password feature at all). `secret_sub` is the identifier for
405
- # use inside a `!Sub` "${...}" interpolation (dot-free for a plain
406
- # `!Ref`, dotted for a `!GetAtt` attribute — CloudFormation's `${}`
407
- # auto-detects either shape); `secret_intrinsic` is the same fact
408
- # spelled as a bare YAML value, for the one site (stack_outputs) that
409
- # isn't inside a `!Sub` string at all.
181
+ # Aurora uses a self-managed secret (ManageMasterUserPassword rotation
182
+ # would break a Lambda's static Environment); plain RDS uses its own
183
+ # MasterUserSecret attribute. `secret_sub` is for `!Sub "${...}"`
184
+ # interpolation; `secret_intrinsic` is the same fact as a bare value.
410
185
  secret_sub = aurora ? "#{db_id}Secret" : "#{db_ref_id}.MasterUserSecret.SecretArn"
411
186
  secret_intrinsic = aurora ? "!Ref #{db_id}Secret" : "!GetAtt #{db_ref_id}.MasterUserSecret.SecretArn"
412
- # The same `${...}`-inside-`!Sub` identifier `secret_sub` above already
413
- # is -- used here for #{logical_id}'s own runtime IAM grant instead of a
414
- # deploy-time `{{resolve:secretsmanager:...}}` dynamic reference (see
415
- # that Environment.Variables comment below for why DATABASE_URL moved
416
- # off that mechanism entirely). Shared mode has no `secret_sub` of its
417
- # own to reach for -- `OwningDatabaseSecretArn` (this template's own
418
- # Parameter, filled from the owning stack's Output) already is the bare
419
- # ARN string, not a Ref/GetAtt target inside this stack.
187
+ # Shared mode has no `secret_sub` of its own; `OwningDatabaseSecretArn`
188
+ # (this template's Parameter) is already a bare ARN string.
420
189
  db_secret_ref = shared ? "OwningDatabaseSecretArn" : secret_sub
421
190
 
422
- # **Opt-in, by file presence** — a domain that wants its own web app on
423
- # Lambda too (not every domain has one; Banking doesn't) drops a
424
- # `lambda_handler.rb` at its own root, the Rack-to-Function-URL-event
425
- # adapter Sinatra runs through. No new .world verb for this: the file
426
- # itself is the declaration, the same way a `.hecksagon` file's mere
427
- # existence is what makes a domain framework-attached.
191
+ # Opt-in by file presence: a domain wanting its own web app on Lambda
192
+ # drops a `lambda_handler.rb` (the Rack-to-Function-URL-event adapter
193
+ # Sinatra runs through) at its own root. No separate .world verb.
428
194
  web_handler_present = File.exist?(File.join(domain, "lambda_handler.rb"))
429
195
 
430
- # A domain declares one web story, not both — `web "Rust"` (rust/host
431
- # serves its own web UI in-process) and a `lambda_handler.rb` (a
432
- # separate Ruby WebFunction) are two different mechanisms for the same
433
- # job; picking one silently when both are present would be exactly the
434
- # kind of ambiguity this generator's own `deploy.bluebook` gate exists
435
- # to refuse instead of guess at.
196
+ # A domain declares one web story, not both.
436
197
  if rust_web && web_handler_present
437
198
  raise ArgumentError, "#{domain} declares both web \"Rust\" (#{world_file}) and a lambda_handler.rb (#{File.join(domain, 'lambda_handler.rb')}) — pick one."
438
199
  end
439
200
 
440
- # Read straight out of the domain's own Gemfile.lock, not hardcoded —
441
- # `patch-pg-native`'s build recipe below has to fetch the same pg
442
- # version Bundler resolved, or its from-source extension and the
443
- # gem's own Ruby wrapper (lib/pg.rb, autoloads, etc.) can drift apart.
444
- # The plain, platform-less lock line ("pg (1.6.3)") is what this
445
- # matches — platform-suffixed siblings ("pg (1.6.3-aarch64-linux)")
446
- # fail the all-digits-and-dots capture on purpose.
447
- #
448
- # Optional, not required whenever a web app exists — Embryonaut's own
449
- # Sinatra app talks to Postgres directly (hence `pg` in its
450
- # Gemfile.lock) but that's that app's own choice, not a rule every
451
- # WebFunction follows. A web app that only ever dispatches through a
452
- # Lambda-routed domain (RemoteDispatcher/Adapters::Lambda, never a
453
- # local Postgres connection — hecks_on_web's own apps, e.g.) has no
454
- # `pg` gem and needs none of the native-extension patching below; the
455
- # generated Makefile's own `deploy:` target branches on whether this
456
- # is present, same convention `google_oauth_present` already uses for
457
- # its own opt-in machinery.
201
+ # Read from the domain's own Gemfile.lock, not hardcoded — the
202
+ # `patch-pg-native` build recipe below must fetch the same pg version
203
+ # Bundler resolved, or the from-source extension can drift from it.
458
204
  pg_version = nil
459
205
  if web_handler_present
460
206
  # `dispatch "None"` reads this project's own root Gemfile.lock, not
461
- # #{domain}'s — WebFunction's own CodeUri is `root` in that case
462
- # (`web_code_uri`, below), not #{domain} itself, precisely because
463
- # #{domain} (a bluebook directory inside this very repo, e.g. "qa")
464
- # has no Gemfile of its own to bundle at all; it needs `lib/hecks`
465
- # and this project's own Gemfile.lock, sitting one level up. Every
466
- # other WebFunction (Embryonaut's own self-contained app checkout,
467
- # vendoring its own copy of hecks) keeps reading its own, unchanged.
207
+ # #{domain}'s: #{domain} has no Gemfile of its own to bundle (it
208
+ # needs `lib/hecks` from one level up), so CodeUri is `root` there.
468
209
  pg_version_path = File.join(dispatch_none ? root : domain, "Gemfile.lock")
469
210
  pg_version = File.read(pg_version_path)[/^\s+pg \(([\d.]+)\)/, 1]
470
211
  end
471
212
  web_logical_id = "WebFunction"
472
213
 
473
- # `dispatch "None"` — same reason `pg_version_path` reaches one
474
- # directory further up, above: #{domain} alone has no Gemfile/lib/hecks
475
- # of its own to zip, so WebFunction's CodeUri has to be this whole
476
- # project's own root instead (the identical absolute-path shape
477
- # Embryonaut's own real, live deploy already uses for a checkout that
478
- # isn't colocated with `deploy/<stack>/` either — bin/project_deploy's
479
- # own header: "Self-contained... no hand-authored deployment config").
480
- # `web_handler_relpath` follows the same split: #{domain}/lambda_handler
481
- # is only the right relative path once CodeUri stops being #{domain}
482
- # itself — every existing WebFunction (CodeUri: #{domain}) still finds
483
- # its own lambda_handler.rb at that directory's own root, unchanged.
214
+ # Same `dispatch "None"` split as `pg_version_path` above: #{domain}
215
+ # alone has no Gemfile/lib/hecks to zip, so CodeUri is `root` instead.
484
216
  web_code_uri = dispatch_none ? root : domain
485
217
  web_handler_relpath = dispatch_none ? "#{domain}/lambda_handler" : "lambda_handler"
486
218
 
487
- # Opt-in, by file presence + content — the same convention
488
- # `web_handler_present` itself uses. `.env.local` is #{domain}'s own
489
- # gitignored local-dev secrets file (never committed, never read by
490
- # this tool for anything but detecting the key is there); the real
491
- # value is synced into Secrets Manager by `make sync-google-oauth`
492
- # below, straight from that same file, at deploy time — never baked
493
- # into this generated, git-tracked template.yaml as plaintext.
494
- # Also gates the NAT Gateway/public subnet template.yaml's own "no NAT
495
- # gateway" comment describes — computed here, once, before
496
- # stack_outputs/bastion_parameters below need to know whether
497
- # #{db_id}PublicSubnet exists at all to pass along to bastion.yaml.
219
+ # `.env.local` is #{domain}'s gitignored local-dev secrets file, read
220
+ # only to detect the key is present; `make sync-google-oauth` syncs
221
+ # the real value into Secrets Manager at deploy time.
498
222
  google_oauth_present = (web_handler_present || rust_web) &&
499
223
  File.exist?(File.join(domain, ".env.local")) &&
500
224
  File.read(File.join(domain, ".env.local")).match?(/^GOOGLE_CLIENT_ID=\S/)
501
225
 
502
- # **The Ruby case stays refused** — a shared-instance Ruby WebFunction
503
- # needs its own cross-stack DATABASE_URL wiring this generator doesn't
504
- # build yet (web_handler_present's own Member-style direct-Postgres
505
- # binding), real, unbuilt design, not a small addition. This check
506
- # alone is why the google_oauth_present check below can assume
507
- # web_handler_present is already false by the time it runs — it always
508
- # aborts first when both are true.
226
+ # A shared-instance Ruby WebFunction needs cross-stack DATABASE_URL
227
+ # wiring this generator doesn't build yet.
509
228
  if shared && web_handler_present
510
229
  raise ArgumentError, "#{domain} declares both database \"Shared\" and a lambda_handler.rb — a shared-instance Ruby WebFunction isn't supported yet."
511
230
  end
512
231
 
513
- # `dispatch "None"` means WebFunction becomes the domain's only Lambda —
514
- # there is nothing left to reach through a Function URL at all without a
515
- # `lambda_handler.rb` to serve it, and (below) no rust_web dispatch
516
- # Lambda whose own Function URL that role could fall back to either.
232
+ # `dispatch "None"` means WebFunction is the domain's only Lambda.
517
233
  if dispatch_none && !web_handler_present
518
234
  raise ArgumentError, "#{domain} declares dispatch \"None\" but has no lambda_handler.rb — dispatch \"None\" means no rust/host dispatch Lambda at all, so a WebFunction (lambda_handler.rb) has to exist to be the domain's only Lambda."
519
235
  end
@@ -521,33 +237,12 @@ module Hecks
521
237
  raise ArgumentError, "#{domain} declares both dispatch \"None\" and web \"Rust\" — dispatch \"None\" already means there is no rust/host Lambda for rust_web's own in-process web UI to run inside."
522
238
  end
523
239
 
524
- # **The Rust case is not refused** — rust_web's own OAuth wiring (the main
525
- # dispatch function's own Environment/VpcConfig, generated below) needs
526
- # no NAT Gateway of its own in Shared mode: it already runs inside the
527
- # owner's borrowed private subnets (OwningSubnetAId/OwningSubnetBId)
528
- # and borrowed security group (OwningSecurityGroupId) — the same ones
529
- # this domain's own dispatch traffic already uses — and the owner's own
530
- # template already routes those subnets through its NAT Gateway and
531
- # already permits 443-to-internet egress on that security group (added
532
- # there for the owner's own WebFunction's real, live OAuth
533
- # token-exchange bug — see that stack's own
534
- # `#{owner_domain_name}FunctionEgressToInternet` resource). Nothing new
535
- # to provision for this combination; the Parameters section below just
536
- # has to declare both the Owning* set (Shared mode) and
537
- # WebRedirectBaseUrl (OAuth) together, not as alternatives — see its
538
- # own comment on why an if/elsif there would be wrong.
539
- #
540
- # Still a real gap, left refused: rust_web isn't checked here
541
- # explicitly because shared && web_handler_present already aborted
542
- # above whenever web_handler_present is true — so by construction, if
543
- # `shared && google_oauth_present` is ever true at this point,
544
- # web_handler_present must be false, meaning google_oauth_present's own
545
- # `(web_handler_present || rust_web)` can only have been satisfied by
546
- # rust_web. Nothing left to refuse here.
547
-
548
- # The stack↔bastion contract, and the least-privilege cross-domain
549
- # invoke grant — `Shared`'s own header explains why both are
550
- # `Fargate`'s problem too, not only this target's.
240
+ # A Shared-mode rust_web domain with OAuth needs no new NAT Gateway:
241
+ # it already runs inside the owner's borrowed private subnets and
242
+ # security group, which already route to the internet for OAuth.
243
+
244
+ # The stack-bastion contract and the least-privilege cross-domain
245
+ # invoke grant are shared with `Fargate` — see `Shared`'s header.
551
246
  stack_outputs = Shared.stack_outputs(
552
247
  shared: shared, db_id: db_id, db_ref_id: db_ref_id, secret_intrinsic: secret_intrinsic,
553
248
  compute_security_group_ref: "!Ref #{logical_id}SecurityGroup", google_oauth_present: google_oauth_present
@@ -555,14 +250,9 @@ module Hecks
555
250
  bastion_parameters = Shared.bastion_parameters(shared: shared, google_oauth_present: google_oauth_present)
556
251
  Shared.check_bastion_parameters!(bastion_parameters, stack_outputs)
557
252
 
558
- # Computed here, as its own local, rather than as two heredocs
559
- # opened side by side inside the big template heredoc's own
560
- # interpolation below — a heredoc's body is read starting from
561
- # the line after it opens, so two on one line only stay
562
- # unambiguous as long as neither branch is ever edited to span
563
- # the other's own territory. An ordinary `if`/`else`, each
564
- # heredoc entirely inside its own branch, cannot develop that
565
- # failure mode at all.
253
+ # A plain local, not two heredocs opened side by side inside the
254
+ # template heredoc's own interpolation — a heredoc body starts on the
255
+ # line after it opens, so an if/else here keeps each one unambiguous.
566
256
  web_google_oauth_env_yaml =
567
257
  if google_oauth_present
568
258
  <<~GOOGLE.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join.rstrip
@@ -619,22 +309,10 @@ module Hecks
619
309
  journal (docs/decisions/0018), backed by its own private RDS
620
310
  Postgres instance. PackageType Zip, no container.
621
311
  #{
622
- # Two independent reasons a Parameters section might be needed --
623
- # google_oauth_present (any web mode) and shared (any domain
624
- # borrowing an owner's VPC) -- are collected into one array and
625
- # joined under a single `Parameters:` header (YAML permits exactly
626
- # one), rather than treated as mutually exclusive alternatives via
627
- # an if/elsif/else here. That shape breaks for the one combination
628
- # both can be true at once (a Shared-mode rust_web domain with
629
- # real Google OAuth): the elsif branch would never run, so
630
- # OwningSubnetAId/OwningSecurityGroupId/etc. would never get declared as
631
- # Parameters at all, even though VpcConfig below (`shared ?
632
- # "SubnetIds: [!Ref OwningSubnetAId, ...]" : ...`) already
633
- # references them unconditionally whenever `shared` is true --
634
- # exactly the CloudFormation-references-an-undeclared-Parameter
635
- # break bastion_parameters' own generation-time assertion (above)
636
- # exists to catch for a different table. This fixes it for both
637
- # the "either" and the "both" case.
312
+ # google_oauth_present and shared are independent and collected
313
+ # into one array under a single `Parameters:` header, not an
314
+ # if/elsif/else — both can be true at once (a Shared-mode
315
+ # rust_web domain with real Google OAuth).
638
316
  param_blocks = []
639
317
  if google_oauth_present
640
318
  param_blocks << <<~OAUTHPARAMS.rstrip
@@ -693,17 +371,8 @@ module Hecks
693
371
  if param_blocks.empty?
694
372
  ""
695
373
  else
696
- # Every line indented 2, not just appended flush-left — each
697
- # heredoc above squiggly-dedents to column 0 (its own
698
- # least-indented line, matching every other heredoc's own
699
- # convention in this file), but these are Parameters: children,
700
- # which YAML requires indented under it. Confirmed the hard way:
701
- # without this, `ruby -ryaml` parses the file without raising
702
- # (it's still syntactically valid YAML) but
703
- # `doc["Parameters"]["OwningVpcId"]` is nil — WebRedirectBaseUrl/
704
- # OwningVpcId/etc. land as their own top-level document keys,
705
- # siblings of Parameters/Resources, not children of Parameters
706
- # at all.
374
+ # Indented 2: the heredocs above dedent to column 0, but these
375
+ # are Parameters: children, which YAML requires indented under it.
707
376
  "Parameters:\n" + param_blocks.join("\n").each_line.map { |l| " #{l}" }.join
708
377
  end
709
378
  }
@@ -805,13 +474,8 @@ module Hecks
805
474
  # sibling of VpcConfig) is the one runtime IAM permission
806
475
  # that fetch needs, scoped to this one secret ARN.
807
476
  #{if shared
808
- # **The storehouse** — #{owner_domain_name}'s own live
809
- # Endpoint/Secret, looked up at deploy time into
810
- # OwningDatabaseEndpoint/OwningDatabaseSecretArn (this
811
- # template's own Parameters, above), not a resource in
812
- # this stack. Same dbname as #{owner_domain_name}'s own
813
- # DBName -- one database, isolated by schema
814
- # (HECKS_SCHEMA below), not a second database.
477
+ # Owner's live Endpoint/Secret, looked up into
478
+ # OwningDatabaseEndpoint/OwningDatabaseSecretArn.
815
479
  "DB_HOST: !Ref OwningDatabaseEndpoint\n" \
816
480
  " DB_NAME: #{owner_db_name}\n" \
817
481
  " DB_SECRET_ARN: !Sub \"${OwningDatabaseSecretArn}\""
@@ -1176,31 +840,13 @@ module Hecks
1176
840
  YAML
1177
841
 
1178
842
  # Spliced in after the heredoc renders, not interpolated inside it —
1179
- # found live, the hard way: `<<~`'s own dedent strips whatever the
1180
- # shallowest leading-whitespace line in the raw source text has, computed
1181
- # before any `#{...}` interpolation runs. `cross_domain_lambda_policies`
1182
- # is "" for the overwhelmingly common case (no cross-domain policies at
1183
- # all), and a `#{...}` marker written at column 0 in the source (so it
1184
- # renders flush when empty) is itself a zero-indent line by that same
1185
- # raw-text measure — dragging the whole heredoc's computed dedent down to
1186
- # zero and leaving every other line's original leading whitespace
1187
- # un-stripped, a real, generated-then-caught-by-spec bug (`bin/
1188
- # project_deploy`'s own contract spec: "Outputs only declares []" — not
1189
- # actually empty, just re-indented into structural nonsense by this).
1190
- # A plain, unconditional `String#sub` after the fact has no such
1191
- # interaction with the text it's replacing into.
843
+ # a `#{...}` marker at column 0 would drag the heredoc's own dedent
844
+ # computation to zero and leave every other line un-stripped.
1192
845
  template_yaml = template_yaml.sub(/^([ \t]*)# TMPL:cross_domain_lambda_policies\n/) { Shared.cross_domain_invoke_policy_yaml(cross_domain_lambda_targets, $1) }
1193
846
 
1194
- # **The `PII` → CloudFront splice** — operates on the already-fully-rendered
1195
- # string, the same reason the `dispatch_none` splice (below) does rather
1196
- # than threading a third reindentation layer through the main heredoc
1197
- # above: `pii_cloudfront_yaml` builds its own already-correctly-indented
1198
- # text from scratch (its own `reindent` lambda), so there is nothing here
1199
- # for a heredoc dedent computation to interact with badly.
1200
- # `fronted_logical_id`/`use_oac` are resolved here, not earlier, because
1201
- # both need `web_handler_present`/`web_logical_id`/`rust_web`/`logical_id`
1202
- # — every one of which is computed after this generator's own pii
1203
- # detection, above.
847
+ # Operates on the already-rendered string — `pii_cloudfront_yaml`
848
+ # builds its own correctly-indented text, so nothing here interacts
849
+ # with a heredoc's own dedent computation.
1204
850
  if pii_detected
1205
851
  fronted_logical_id = web_handler_present ? web_logical_id : logical_id
1206
852
  use_oac = !web_handler_present && !rust_web
@@ -1212,46 +858,22 @@ module Hecks
1212
858
  template_yaml = template_yaml.sub(/^Outputs:\n/) { "#{pii_resources}Outputs:\n PiiDistributionDomainName:\n Value: !GetAtt PiiDistribution.DomainName\n" }
1213
859
  end
1214
860
 
1215
- # `dispatch "None"` — surgical removal, POST-render, rather than a
1216
- # fourth reindentation layer threaded through the heredoc above.
1217
- # Tried that first: wrapping `#{logical_id}`'s own resource block in a
1218
- # conditional nested heredoc (matching `RUSTSECRET`/`WEB`/`OWNDB`'s own
1219
- # established `<<~TAG.each_line.with_index.map { ... }` convention)
1220
- # looked right, but broke several pre-existing embedded multi-line
1221
- # `#{shared ? "a\nb" : "c"}`-shaped strings already living inside that
1222
- # block (the DB_HOST/DB_NAME/DB_SECRET_ARN Environment lines among
1223
- # them) — those assume they sit at exactly one reindentation layer deep
1224
- # (the outer template heredoc's own margin), and adding a second layer
1225
- # on top shifted every line their own hardcoded embedded-newline
1226
- # indentation didn't already account for. Confirmed live, diffing a
1227
- # real regenerated examples/banking (`dispatch` unset — the ordinary,
1228
- # far more common path) against its own git-tracked output: real
1229
- # indentation corruption, not a false alarm. Operating on the already-
1230
- # fully-rendered, already-correctly-indented string instead sidesteps
1231
- # that whole class of interaction — no new heredoc layer, so nothing
1232
- # already living inside the old one has to change its own assumptions.
861
+ # Surgical removal, post-render, not a nested heredoc — a conditional
862
+ # nested heredoc broke embedded multi-line `#{shared ? ... : ...}`
863
+ # strings inside the resource block, which assume exactly one
864
+ # reindentation layer (confirmed regenerating examples/banking).
1233
865
  if dispatch_none
1234
- # The #{logical_id} resource itself — non-greedy through its own
1235
- # `FunctionUrlConfig`/`AuthType` closing pair (the actual last
1236
- # property this resource ever renders, confirmed above), so a second
1237
- # occurrence of "AuthType:" later in the document (WebFunction's own,
1238
- # always AuthType: `NONE`) can never be matched instead.
866
+ # Non-greedy through the resource's own FunctionUrlConfig/AuthType
867
+ # closing pair, so WebFunction's own later "AuthType:" can't match.
1239
868
  template_yaml = template_yaml.sub(
1240
869
  /^ #{Regexp.escape(logical_id)}:\n.*?\n AuthType: (?:NONE|AWS_IAM)\n/m, ""
1241
870
  )
1242
- # WebFunction's own Policies — `LambdaInvokePolicy: FunctionName: !Ref
1243
- # #{logical_id}` grants invoking a function that, above, was just
1244
- # removed from this template entirely; SAM refuses a `!Ref` to an
1245
- # undeclared resource at package time, not silently.
871
+ # Removes the now-dangling `!Ref` to the resource just deleted above
872
+ # — SAM refuses an unresolved Ref at package time, not silently.
1246
873
  template_yaml = template_yaml.sub(
1247
874
  /^ - LambdaInvokePolicy:\n FunctionName: !Ref #{Regexp.escape(logical_id)}\n/, ""
1248
875
  )
1249
- # Outputs — no `#{logical_id}Url` exists for `FunctionUrl` to
1250
- # `!GetAtt`; WebFunction's own URL becomes the stack's one and only
1251
- # "FunctionUrl" (never "WebFunctionUrl" — that name is reserved for
1252
- # the two-URL case, a domain that also has a #{logical_id} of its
1253
- # own to disambiguate from, which this one, by construction, never
1254
- # does).
876
+ # WebFunction's own URL becomes the stack's only "FunctionUrl".
1255
877
  template_yaml = template_yaml.sub(
1256
878
  /^ FunctionUrl:\n Value: !GetAtt #{Regexp.escape(logical_id)}Url\.FunctionUrl\n WebFunctionUrl:\n Value: !GetAtt #{Regexp.escape(web_logical_id)}Url\.FunctionUrl\n/,
1257
879
  " FunctionUrl:\n Value: !GetAtt #{web_logical_id}Url.FunctionUrl\n"
@@ -1260,42 +882,18 @@ module Hecks
1260
882
 
1261
883
 
1262
884
 
1263
- # **The ephemeral era-minting bastion** — a separate, sibling stack
1264
- # (`#{stack_name}-bastion`), never merged into template.yaml itself.
1265
- # The main stack stays exactly as minimal as its own header already
1266
- # commits to (no NAT gateway, no bastion sitting there costing money
1267
- # by default) — this template only ever exists for the few minutes
1268
- # `make mint-era` (below) needs it, then gets deleted. SSM Session
1269
- # Manager only, never SSH: no key pair, no inbound security group rule
1270
- # at all (the instance's own SG declares zero Ingress) — the only way
1271
- # in is `aws ssm start-session`, itself gated by the caller's own IAM
1272
- # permissions, nothing this template opens to the internet.
1273
- # Skipped entirely when `shared` — this domain provisions no RDS/VPC
1274
- # of its own for a bastion to reach (bastion_parameters is empty for
1275
- # the same reason, above); a bare `!Ref VpcId` with no declared
1276
- # Parameter would just fail at deploy time. Era-minting for a
1277
- # Shared-mode domain reuses its owner's own already-standing
1278
- # infrastructure instead — see the Makefile's own `mint-era` comment
1279
- # for the manual path this leaves until that's automated too.
885
+ # A separate, sibling stack for the few minutes `make mint-era`
886
+ # needs it, then deleted — SSM Session Manager only, never SSH, no
887
+ # inbound rule at all. Skipped when `shared`: era-minting reuses the
888
+ # owner's own standing infrastructure instead.
1280
889
  bastion_yaml = shared ? nil : Shared.bastion_yaml(
1281
890
  domain: domain, infra_name: infra_name, stack_name: stack_name, db_id: db_id,
1282
891
  google_oauth_present: google_oauth_present, bastion_parameters: bastion_parameters
1283
892
  )
1284
893
 
1285
- # `mint-era`'s own recipe body — a top-level variable, not inlined at
1286
- # its call site inside the Makefile heredoc below, on purpose: Make
1287
- # recipe lines need a literal tab as their true first character (no
1288
- # leading spaces at all), and nesting this heredoc a second level
1289
- # deeper inside another `#{...}` interpolation (the way `OWNDB`/params
1290
- # above handle conditional template.yaml content) would need its own
1291
- # per-line re-indent pass — one that adds spaces before every line
1292
- # including the already-tab-prefixed recipe ones, corrupting Make's
1293
- # own recipe-line detection. A single top-level heredoc, each line
1294
- # already at its own final indentation (recipe lines as " \t..." —
1295
- # the same two-space-before-the-tab convention every other recipe
1296
- # line in this Makefile already uses, stripped by the encolosing
1297
- # `<<~MAKE` heredoc's own squiggly stripping below, same as them),
1298
- # sidesteps that: nothing re-touches an already-correct line twice.
894
+ # A top-level variable, not inlined in the Makefile heredoc below —
895
+ # Make recipe lines need a literal leading tab, and a nested
896
+ # interpolation's re-indent pass would corrupt that detection.
1299
897
  mint_era_recipe =
1300
898
  if shared
1301
899
  <<~SHAREDMINT.rstrip
@@ -1474,42 +1072,15 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1474
1072
  OWNMINT
1475
1073
  end
1476
1074
 
1477
- # `scaffold-translation`/`translation-audit` — the two-step fix
1478
- # minter.rb's own refusal names ("run bin/scaffold_translation to write
1479
- # the edge, check it with bin/translation_audit, then boot again") when
1480
- # a deploy's own pre-flight boot check refuses a shape change with no
1481
- # translation edge covering it. Same bastion/tunnel/retry/teardown
1482
- # chain mint_era_recipe already uses — only the one thing done over the
1483
- # tunnel differs: these run the scaffold/audit scripts (hecks's
1484
- # own bin/, not a Ruby -e one-liner) instead of a boot check, since
1485
- # both scripts already do their own registry-loading internally.
1075
+ # Shares mint_era_recipe's bastion/tunnel/retry/teardown chain but
1076
+ # runs bin/scaffold_translation or bin/translation_audit over it.
1486
1077
  #
1487
- # HECKS_SCHEMA/DATABASE_URL below are not actually consumed by those
1488
- # scripts, despite reading as if they were: grep-confirmed, neither
1489
- # `bin/scaffold_translation` nor `bin/translation_audit` nor
1490
- # `Hecks::Bluebook::Behaviour::World#for_binding` nor
1491
- # `PostgresEra.connect_for` ever reads `ENV["DATABASE_URL"]` or
1492
- # `ENV["HECKS_SCHEMA"]` — both scripts call
1493
- # `registry.binding_settings(...)`, a pure hash lookup
1494
- # against whatever literal `database "..."` string #{domain}'s own
1495
- # `.world` file declares. Without `db_env_blind:` below, this whole
1496
- # recipe would stand up a real bastion, punch a live 5432 ingress rule
1497
- # into production's security group, open a real SSM tunnel to
1498
- # #{stack_name}'s RDS instance, tear it all down again — and then
1499
- # silently scaffold/audit the developer's local dev database the
1500
- # entire time, reporting success. `db_env_blind:`
1501
- # (below) is exactly this: true for scaffold-translation/
1502
- # translation-audit (confirmed env-blind), left false for
1503
- # migrate-console-settings (an app-owned script this generator doesn't
1504
- # control the internals of — it may honor these vars; not asserting
1505
- # either way here). A `db_env_blind` recipe refuses before ever
1506
- # touching AWS unless `ALLOW_LOCAL_DB=1` is set, rather than silently
1507
- # doing the wrong (but locally successful-looking) thing.
1508
- # `cwd:`/`run_prefix:` — every existing caller runs a hecks-owned
1509
- # script from hecks's own $(root) with `-Ilib` (source, not the
1510
- # installed gem); `migrate_console_settings_recipe` below is the one
1511
- # exception, an app-owned script that needs the app's own Gemfile
1512
- # context instead — `cd $(DOMAIN) && bundle exec ruby`, not `-Ilib`.
1078
+ # Neither script actually reads DATABASE_URL/HECKS_SCHEMA (both call
1079
+ # `registry.binding_settings`, a lookup against the literal `database
1080
+ # "..."` in #{domain}'s `.world`) — without `db_env_blind:`, this
1081
+ # recipe would open a real tunnel to production and silently
1082
+ # scaffold/audit the local dev database instead, reporting success.
1083
+ # `db_env_blind` refuses unless `ALLOW_LOCAL_DB=1` is set.
1513
1084
  translation_recipe = lambda do |verb, script, extra_args = "", cwd: "$(ROOT)", run_prefix: "ruby -Ilib", db_env_blind: false|
1514
1085
  if shared
1515
1086
  <<~SHAREDTRANSLATION.rstrip
@@ -1577,38 +1148,18 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1577
1148
  scaffold_translation_recipe = translation_recipe.call("scaffold-translation", "bin/scaffold_translation", db_env_blind: true)
1578
1149
  translation_audit_recipe = translation_recipe.call("translation-audit", "bin/translation_audit", db_env_blind: true)
1579
1150
 
1580
- # `make migrate-console-settings` — the same bastion/tunnel/retry/
1581
- # teardown chain as scaffold-translation/translation-audit, running an
1582
- # app-owned one-time migration script instead of one of hecks's
1583
- # own bin/ tools (see translation_recipe's own `cwd:`/`run_prefix:`
1584
- # comment). Only meaningful for a domain that actually has one — most
1585
- # domains don't, so this target is generated unconditionally but simply
1586
- # has nothing to run for them; harmless (`bundle exec ruby` on a
1587
- # missing file just fails loudly, same as any other missing script
1588
- # would).
1151
+ # Same chain, running an app-owned one-time migration script instead
1152
+ # of a hecks bin/ tool. Harmless when the domain has none: `bundle
1153
+ # exec ruby` on a missing file just fails loudly.
1589
1154
  migrate_console_settings_recipe = translation_recipe.call(
1590
1155
  "migrate-console-settings", "bin/migrate_console_settings", "",
1591
1156
  cwd: "$(DOMAIN)", run_prefix: "bundle exec ruby"
1592
1157
  )
1593
1158
 
1594
- # `rename-schema`'s own recipe body — same top-level-variable-not-inlined
1595
- # reasoning as `mint_era_recipe` just above (recipe lines need a literal
1596
- # leading tab; nesting this heredoc a level deeper would need its own
1597
- # re-indent pass). Reuses the same bastion.yaml, the same stack/
1598
- # BASTION_STACK Make variables mint-era already defines, and the same
1599
- # stand-up/tunnel/teardown-no-matter-what shell chain — only the one
1600
- # thing done over the tunnel differs: a schema rename instead of a
1601
- # Ruby boot. `OLD`/`NEW` are Make command-line variables
1602
- # (`make rename-schema OLD=old NEW=new`), not baked in here, so this
1603
- # is genuinely reusable — the domain whose own schema this renames is
1604
- # whichever one owns this stack's RDS instance (this generator's own
1605
- # `#{infra_name}` database on it), not necessarily this domain forever.
1606
- #
1607
- # Idempotent by inspection, not by catching a Postgres error: checks
1608
- # which of old/new actually exists as a schema before touching
1609
- # anything, so a second run (or a run after a partial failure) reads
1610
- # as a clear no-op message rather than a bare "schema already exists"
1611
- # error with no context.
1159
+ # `OLD`/`NEW` are Make command-line variables (`make rename-schema
1160
+ # OLD=old NEW=new`), reusable across whichever domain owns this
1161
+ # stack's RDS instance. Idempotent by inspection (checks which schema
1162
+ # exists first), not by catching a Postgres error.
1612
1163
  rename_schema_recipe =
1613
1164
  if shared
1614
1165
  <<~SHAREDRENAME.rstrip
@@ -1718,27 +1269,11 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1718
1269
  OWNRENAME
1719
1270
  end
1720
1271
 
1721
- # `deploy:`'s own `sam deploy` call — `google_oauth_present` and
1722
- # `shared` are collected together here rather than spelled as an
1723
- # if/elsif/else, the same shape `Parameters:` above holds to: the
1724
- # two are independent facts about a domain, not alternatives, and
1725
- # a client site can be both — an elsif shape would silently drop the
1726
- # Owning* overrides whenever OAuth is also present. Caught live
1727
- # the first time a Shared-mode domain with real Google OAuth
1728
- # actually ran `make deploy`: `sam deploy` refused
1729
- # with "Parameters: [OwningVpcId, ...] must have values" because
1730
- # nothing had ever passed them. WebRedirectBaseUrl stays a conditional
1731
- # override (empty on a genuine first deploy, before the Function Url
1732
- # exists — see that Parameter's own comment); Owning* is unconditional
1733
- # whenever `shared`, in every branch that reaches `sam deploy` at all.
1734
- # One continuous `\`-joined shell chain, however many pieces
1735
- # contribute to it — `@` (Make's own "don't echo this line" prefix)
1736
- # is only meaningful on the true first line of that chain; written on
1737
- # any later line it stops being a Make directive at all and becomes
1738
- # literal shell text (`@WEB_URL=...` parses as a bogus command, not
1739
- # an assignment) the moment two independently-`@`-prefixed pieces
1740
- # get concatenated. Built unprefixed here; `@` is added once, to
1741
- # whichever piece actually ends up first, right before joining.
1272
+ # `google_oauth_present` and `shared` are independent facts, not
1273
+ # alternatives — an elsif would silently drop Owning* overrides
1274
+ # whenever OAuth is also present. `@` (Make's "don't echo" prefix)
1275
+ # is only meaningful on the chain's true first line, so lines are
1276
+ # built unprefixed and `@` is added once, right before joining.
1742
1277
  owner_lookup_lines = shared ? [
1743
1278
  %(OWNER_VPC_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='VpcId'].OutputValue" --output text); \\),
1744
1279
  %(OWNER_SUBNET_A_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='PrivateSubnetAId'].OutputValue" --output text); \\),
@@ -1777,36 +1312,13 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1777
1312
  deploy_shell_chain = owner_lookup_lines + sam_deploy_lines
1778
1313
  deploy_shell_chain = ["@#{deploy_shell_chain.first}"] + deploy_shell_chain.drop(1) if deploy_shell_chain.first&.match?(/=\$\$\(/)
1779
1314
 
1780
- # `deploy:`'s own pre-deploy `mint-era` bridge (below, inlined into
1781
- # `PREDEPLOYBRIDGE`) — a plain Ruby string built here, not inlined
1782
- # directly in that heredoc, for the same reason as everywhere else
1783
- # comment: it needs its own `if/else` branch on `google_oauth_present`,
1784
- # and Ruby heredoc-in-string-interpolation only reads cleanly one level
1785
- # deep before it gets hard to follow.
1786
- #
1787
- # Google OAuth newly added to an existing stack deadlocks this bridge —
1788
- # confirmed live: stack_outputs (above) only gains PublicSubnetId/
1789
- # BastionSubnetId entries once `google_oauth_present` is true, but those
1790
- # two CloudFormation resources (#{db_id}PublicSubnet/
1791
- # #{db_id}BastionPublicSubnet) don't exist on a stack that predates
1792
- # turning OAuth on — only the upcoming `sam deploy` (further below,
1793
- # still ahead of us here) creates them. This pre-check's own `mint-era`
1794
- # call evaluates stack_outputs against the currently live stack (an
1795
- # `aws cloudformation describe-stacks` eval chain, same one mint-era's
1796
- # own recipe uses), gets an empty string back for both, and hands
1797
- # bastion.yaml an empty `AWS::EC2::Subnet::Id` — CloudFormation refuses
1798
- # that outright. The pre-check fails before `sam deploy` ever runs, so
1799
- # the one deploy that would actually create those outputs never gets
1800
- # the chance to: a hard deadlock, `make deploy` can never get OAuth
1801
- # provisioned on a stack that didn't have it already. Detected the same
1802
- # way a genuine first deploy is detected just below (describe-stacks
1803
- # succeeding or not) — here, describe-stacks succeeds (the stack
1804
- # itself exists) but the specific output this deploy is newly adding
1805
- # does not, yet. Skip the pre-deploy bridge in exactly that one case;
1806
- # the unconditional `mint-era` call at the very end of `deploy:` still
1807
- # covers it once `sam deploy` has actually created PublicSubnetId/
1808
- # BastionSubnetId — the exact same "runs once, after the stack exists"
1809
- # path a domain's true first deploy already takes below.
1315
+ # Google OAuth newly added to an existing stack would deadlock this
1316
+ # bridge: PublicSubnetId/BastionSubnetId don't exist on the live stack
1317
+ # until the upcoming `sam deploy` creates them, so the pre-check's own
1318
+ # `mint-era` call would hand bastion.yaml an empty Subnet::Id and
1319
+ # CloudFormation would refuse it before `sam deploy` ever runs. Skip
1320
+ # the bridge in exactly that case; the unconditional `mint-era` call
1321
+ # at the end of `deploy:` still covers it afterward.
1810
1322
  predeploy_bridge_shell =
1811
1323
  if google_oauth_present
1812
1324
  <<~OAUTHBRIDGE.rstrip
@@ -2214,13 +1726,8 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
2214
1726
  \t$(MAKE) mint-era
2215
1727
  MAKE
2216
1728
 
2217
- # Everything `sam deploy --guided` asks interactively — stack name,
2218
- # region, capabilities, rollback behavior — is already known once the
2219
- # domain and its deployed_to("AwsLambda") block are: none of it needs
2220
- # a human typing answers on every deploy. There's no secret to carry
2221
- # here either now: DATABASE_URL is composed inside the template itself
2222
- # from RDS's own auto-generated Secrets Manager password, so nothing
2223
- # gets typed, saved, or passed as a --parameter-overrides flag at all.
1729
+ # Everything `sam deploy --guided` would ask interactively is already
1730
+ # known from the domain's own `deployed_to("AwsLambda")` block.
2224
1731
  samconfig_toml = <<~TOML
2225
1732
  # GENERATED by bin/project_deploy #{domain} — re-run it to refresh
2226
1733
  # this file rather than hand-editing.
@@ -2243,46 +1750,11 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
2243
1750
  files
2244
1751
  end
2245
1752
 
2246
- # Builds the CloudFront/WAFv2/logging resources a `PII`-marked domain gets fronted by.
2247
- #
2248
- # **`PII` → CloudFront** — once a domain marks a field "pii", its own public
2249
- # surface (`WebFunction` when one exists, `#{logical_id}` otherwise —
2250
- # `fronted_logical_id`, computed where both are known, in `call`) gets
2251
- # fronted by a distribution carrying a WAFv2 WebACL (AWS managed rule
2252
- # groups), security response headers, geo-restriction, and access
2253
- # logging — every other domain's own template is untouched
2254
- # (`pii_detected` false means `call` never invokes this at all).
2255
- #
2256
- # **`OAC` only for the AWS_IAM case** (`use_oac`) — the already-public
2257
- # `WebFunction`/`rust_web` shape (`AuthType: NONE`) is left exactly as
2258
- # reachable as it already was; CloudFront adds WAF/headers/geo/logging
2259
- # on top of that, it does not change who could already call the
2260
- # Function URL directly. `#{logical_id}` itself (the AWS_IAM,
2261
- # internal-dispatch default) is the opposite: OAC lets CloudFront sign
2262
- # requests to it via SigV4 while `PiiLambdaInvokePermission`'s own
2263
- # `SourceArn` admits only this one distribution — direct, unsigned
2264
- # access to the Function URL stays refused exactly as it was before
2265
- # this ran.
2266
- #
2267
- # **Managed cache/origin-request policy ids** — not custom resources.
2268
- # `4135ea2d-6df8-44a3-9df3-4b5a84be39ad`/`216adef6-5c7f-47e4-b989-
2269
- # 5492eafa07d3` are AWS's own permanent, account-independent
2270
- # `Managed-CachingDisabled`/`Managed-AllViewer` ids (the same ones the
2271
- # console's own dropdown offers) — this fronts a Lambda dispatch
2272
- # endpoint, not a static site; caching a response meant for exactly
2273
- # one caller would be a real correctness bug, not a performance choice
2274
- # made once here.
2275
- #
2276
- # @param fronted_logical_id [String] the Lambda resource this distribution fronts
2277
- # @param use_oac [Boolean] true only when `fronted_logical_id`'s own FunctionUrlConfig is
2278
- # AWS_IAM (never true for WebFunction/rust_web, both always `NONE`)
2279
- # @param geo_restriction_type ["none", "allowlist", "blocklist"] `deployed_to`'s own
2280
- # `geo_restriction` setting; "none" (no restriction, structurally present so a later
2281
- # change is a one-line `deployed_to` edit, not a template rewrite) when unset
2282
- # @param geo_restriction_countries [Array<String>] ISO 3166-1 alpha-2 codes; ignored when
2283
- # `geo_restriction_type` is "none"
2284
- # @return [String] the Resources entries to splice in before Outputs:, absolutely
2285
- # indented to 2 spaces (this stack's own top-level Resources entry column)
1753
+ # Builds the CloudFront/WAFv2/logging resources a pii-marked domain is
1754
+ # fronted by. `use_oac` signs requests only for the AWS_IAM case; the
1755
+ # already-public WebFunction/rust_web Function URL stays as reachable
1756
+ # as before. Managed cache/origin-request policy ids are AWS's own
1757
+ # permanent `Managed-CachingDisabled`/`Managed-AllViewer` ids.
2286
1758
  def pii_cloudfront_yaml(fronted_logical_id:, use_oac:, geo_restriction_type:, geo_restriction_countries:)
2287
1759
  reindent = ->(text) { text.each_line.map { |line| line.strip.empty? ? line : " #{line}" }.join }
2288
1760