hecks 3.2.0 → 3.3.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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/check_era.rb +3 -39
  10. data/lib/hecks/cli/interview_draft.rb +86 -31
  11. data/lib/hecks/cli/interview_session.rb +77 -26
  12. data/lib/hecks/cli/interview_shape.rb +179 -0
  13. data/lib/hecks/doors/cli_runner.rb +14 -1
  14. data/lib/hecks/doors/launcher_options.rb +13 -3
  15. data/lib/hecks/doors/mcp_door.rb +16 -2
  16. data/lib/hecks/doors/mcp_door_scope.rb +45 -5
  17. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  18. data/lib/hecks/doors.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  20. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  21. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  22. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  23. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  24. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  25. data/lib/hecks/gate/stages.yml +113 -8
  26. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  28. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  29. data/lib/hecks/hecks/adapters/git.rb +59 -0
  30. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  31. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  32. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  33. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  34. data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
  35. data/lib/hecks/hecks/codebase.behaviors +6 -0
  36. data/lib/hecks/hecks/codebase.bluebook +21 -0
  37. data/lib/hecks/hecks/custodian.behaviors +6 -0
  38. data/lib/hecks/hecks/custodian.bluebook +217 -0
  39. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  40. data/lib/hecks/hecks/hecks.world +8 -3
  41. data/lib/hecks/hecks/site.world +5 -2
  42. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  43. data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
  44. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  45. data/lib/hecks/language/oidc.json +5 -0
  46. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  47. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  48. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  49. data/lib/hecks/projections/deploy/box/settings.rb +40 -6
  50. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  51. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  52. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  53. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  54. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  55. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  56. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  57. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  58. data/lib/hecks/projections/deploy/box.rb +28 -4
  59. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  60. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  61. data/lib/hecks/projections/model/deviations.rb +1 -1
  62. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  63. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  64. data/lib/hecks/projections/site/edge.rb +25 -11
  65. data/lib/hecks/projections/site/live_edge.rb +162 -0
  66. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  67. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  68. data/lib/hecks/projections/site/table/checks.rb +53 -7
  69. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  70. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  71. data/lib/hecks/projections/site/table.rb +16 -5
  72. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  73. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  74. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  75. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  76. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  77. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  78. data/lib/hecks/runtime/interpreting.rb +3 -22
  79. data/lib/hecks/runtime/needs.rb +48 -0
  80. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  81. data/lib/hecks/runtime/registry/verification.rb +3 -3
  82. data/lib/hecks/rust_build/host.rb +147 -0
  83. data/lib/hecks/rust_build/kernel_input.rb +51 -17
  84. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  85. data/lib/hecks/rust_build/project_rust.rb +7 -140
  86. data/lib/hecks/rust_build.rb +1 -0
  87. data/lib/hecks/site/bluebook/site.bluebook +80 -14
  88. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  89. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  90. data/lib/hecks/storehouse.rb +35 -1
  91. data/lib/hecks/tools/comment_style.rb +4 -2
  92. data/lib/hecks/tools/gate.rb +1 -1
  93. data/lib/hecks/tools/rust_comment_style.rb +2 -4
  94. data/lib/hecks/tools/site_routes.rb +69 -28
  95. data/lib/hecks/tools/tools_doc.rb +204 -0
  96. data/lib/hecks/tools.rb +1 -0
  97. data/lib/hecks/version.rb +1 -1
  98. data/lib/hecks/vocabulary.rb +152 -1
  99. data/lib/hecks.rb +1 -0
  100. data/qa/settings.yml +1 -2
  101. data/rust/build/src/lineage_pass.rs +31 -16
  102. data/rust/build/src/main.rs +4 -2
  103. data/rust/build/src/pipeline.rs +9 -20
  104. data/rust/codegen/src/attr.rs +1 -1
  105. data/rust/codegen/src/bridging.rs +8 -8
  106. data/rust/codegen/src/commands.rs +1 -1
  107. data/rust/codegen/src/constraints.rs +1 -1
  108. data/rust/codegen/src/dependency_planning.rs +1 -1
  109. data/rust/codegen/src/domain_generator.rs +2 -2
  110. data/rust/codegen/src/exemplar.rs +1 -1
  111. data/rust/codegen/src/expr_emitter.rs +1 -1
  112. data/rust/codegen/src/fielded.rs +1 -1
  113. data/rust/codegen/src/json.rs +214 -23
  114. data/rust/codegen/src/json_codec.rs +1 -1
  115. data/rust/codegen/src/literal.rs +6 -3
  116. data/rust/codegen/src/main.rs +23 -13
  117. data/rust/codegen/src/mutations.rs +1 -1
  118. data/rust/codegen/src/naming.rs +5 -3
  119. data/rust/codegen/src/optional_pass.rs +226 -0
  120. data/rust/codegen/src/ports.rs +1 -1
  121. data/rust/codegen/src/queries.rs +3 -3
  122. data/rust/codegen/src/reactions.rs +1 -1
  123. data/rust/codegen/src/read_models.rs +2 -2
  124. data/rust/codegen/src/reference_specs.rs +1 -1
  125. data/rust/codegen/src/registry.rs +4 -4
  126. data/rust/codegen/src/shared.rs +1 -1
  127. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  128. data/rust/codegen/src/skip_reason.rs +1 -1
  129. data/rust/codegen/src/types.rs +4 -4
  130. data/rust/host/HECKS_RELEASE +1 -1
  131. data/rust/host/src/api.rs +1 -1
  132. data/rust/host/src/dispatch.rs +5 -0
  133. data/rust/host/src/needs.rs +193 -29
  134. data/rust/host/src/resend.rs +4 -4
  135. data/rust/host/src/ui_schema.rs +1 -1
  136. data/rust/parser/src/emit.rs +12 -0
  137. data/rust/parser/src/ir.rs +2 -0
  138. data/rust/parser/src/keywords.rs +2 -0
  139. data/rust/parser/src/main.rs +1 -0
  140. data/rust/parser/src/parse/command.rs +2 -44
  141. data/rust/parser/src/parse/mod.rs +1 -0
  142. data/rust/parser/src/parse/needs.rs +60 -0
  143. data/rust/parser/src/parse/query.rs +3 -0
  144. data/rust/src/exemplar/commands.rs +1 -1
  145. data/rust/src/exemplar/constraints.rs +1 -1
  146. data/rust/src/exemplar/fielded.rs +1 -1
  147. data/rust/src/exemplar/json.rs +1 -1
  148. data/rust/src/exemplar/mod.rs +1 -1
  149. data/rust/src/exemplar/mutations.rs +1 -1
  150. data/rust/src/exemplar/queries.rs +1 -1
  151. data/rust/src/exemplar/reactions.rs +1 -1
  152. data/rust/src/exemplar/read_models.rs +1 -1
  153. data/rust/src/exemplar/registry.rs +1 -1
  154. data/rust/src/exemplar/types.rs +1 -1
  155. data/rust/src/kernel/cli.rs +18 -8
  156. data/rust/src/kernel/needs.rs +104 -9
  157. data/rust/src/kernel/read_model.rs +1 -1
  158. data/rust/src/lib.rs +1 -1
  159. metadata +24 -30
  160. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  161. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  162. data/lib/hecks/three_zero/forms.yml +0 -127
  163. data/lib/hecks/three_zero.rb +0 -12
  164. data/rust/build/src/json.rs +0 -421
  165. data/rust/build/src/optional_pass.rs +0 -150
  166. data/rust/project/bridging.rb +0 -257
  167. data/rust/project/commands.rb +0 -877
  168. data/rust/project/constraints.rb +0 -86
  169. data/rust/project/dependency_planning.rb +0 -178
  170. data/rust/project/domain_generator.rb +0 -983
  171. data/rust/project/exemplar.rb +0 -148
  172. data/rust/project/expr_emitter.rb +0 -72
  173. data/rust/project/fielded.rb +0 -141
  174. data/rust/project/json_codec.rb +0 -641
  175. data/rust/project/mutations.rb +0 -695
  176. data/rust/project/naming.rb +0 -223
  177. data/rust/project/ports.rb +0 -65
  178. data/rust/project/queries.rb +0 -493
  179. data/rust/project/reactions.rb +0 -317
  180. data/rust/project/read_models.rb +0 -690
  181. data/rust/project/reference_specs.rb +0 -27
  182. data/rust/project/registry.rb +0 -396
  183. data/rust/project/skip_reason.rb +0 -29
  184. data/rust/project/types.rb +0 -225
  185. data/rust/project/write_if_changed.rb +0 -7
  186. data/rust/project.rb +0 -30
  187. data/rust/project_rust_pipeline.rb +0 -391
@@ -625,16 +625,229 @@ Hecks.bluebook "Bluebook" do
625
625
  pattern: '^lib/hecks/runtime/', push: "skip",
626
626
  label: "does this change touch lib/hecks/runtime/**?"
627
627
  member name: "postgres_io_relevant_changed", workflow: "ci-postgres-io-parallel.yml", mode: "skips_unless",
628
- pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests|project)/|rust/project\.rb$|rust/project_rust_pipeline\.rb$|\.rubocop\.yml$|\.rubocop_todo\.yml$|\.mcp\.json$|\.rspec-local\.example$|README\.md$|CHANGELOG\.md$|CONTRIBUTING\.md$|SECURITY\.md$|LICENSE$|\.gitignore$)',
628
+ pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\.rubocop\.yml$|\.rubocop_todo\.yml$|\.mcp\.json$|\.rspec-local\.example$|README\.md$|CHANGELOG\.md$|CONTRIBUTING\.md$|SECURITY\.md$|LICENSE$|\.gitignore$)',
629
629
  push: "before_sha",
630
630
  label: "does this change touch anything rspec_postgres_io_parallel covers?"
631
631
  end
632
632
 
633
+ # The dials of the hecks_qa loop: what each is called, the type of value it takes and what it
634
+ # does. qa/settings.yml holds the values, which are edited and reviewed as a diff; this table
635
+ # holds the rest, so the loader (`Hecks::Fuzzing::QaSettings`) checks the file against it and
636
+ # the QualityControl chapter reads each value as a constant (`QualityControlDials::PR_CAP_PER_DAY`
637
+ # for `pr_cap_per_day`) without a second list of the names or their types. `type` is Integer,
638
+ # Numeric (an Integer or a Float), Boolean, String, Array or Hash. A dial's nested shape, where it
639
+ # has one, is checked by the loader.
640
+ value_object "QaDial" do
641
+ attribute :name, String
642
+ attribute :type, String
643
+ attribute :meaning, String
644
+
645
+ member name: "cadence_seconds", type: "Integer",
646
+ meaning: "Minimum gap in seconds between one tick's report and the next dispatch; 0 is immediately. Read by the `/loop hecks_qa` caller, not by any script."
647
+ member name: "pr_cap_per_day", type: "Integer",
648
+ meaning: "Most PRs (Patch plus Improvement) `hecks quality_control patch.open` opens per calendar day; 0 is uncapped."
649
+ member name: "widening_tiers", type: "Array",
650
+ meaning: "Fuzz depth by `Target.clean_streak`, in three discrete tiers, read by `SweepDepth.for_streak`. The last row (`upto: Float::INFINITY`) is the ceiling."
651
+ member name: "sweep_max_parallel", type: "Integer",
652
+ meaning: "How many `hecks quality_control query sweep.run <target>` children `--all` keeps running at once."
653
+ member name: "liveness_fallback_seconds", type: "Integer",
654
+ meaning: "How long the `/loop hecks_qa` caller waits for a tick's report before assuming it stalled."
655
+ member name: "draft_only", type: "Boolean",
656
+ meaning: "Whether `hecks quality_control patch.open` opens its PR as a draft; false opens it ready for review."
657
+ member name: "auto_merge", type: "Boolean",
658
+ meaning: "Whether `hecks quality_control patch.open` queues the PR for auto-merge once it is open."
659
+ member name: "adversarial_fraction", type: "Numeric",
660
+ meaning: "Fraction (0.0..1.0) of generated command steps mutated adversarially; 0 turns the layer off. Higher finds refusal-ordering splits sooner but reaches deep state less often."
661
+ member name: "guided_generation", type: "Boolean",
662
+ meaning: "Coverage-guided generation across one sweep's seeds (`Hecks::Fuzzing::CoverageCampaign`)."
663
+ member name: "corpus_splice_probability", type: "Numeric",
664
+ meaning: "Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage. Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost."
665
+ member name: "favor_rare_verbs", type: "Integer",
666
+ meaning: "How many of the least-hit verbs the next seed weights up; 0 turns the lever off."
667
+ member name: "self_consistency_checks", type: "Boolean",
668
+ meaning: "Whether `hecks quality_control query sweep.run` also runs the self-consistency pass (cold rehydration, replay idempotency, value-object JSON round trip) on the same generated sequence."
669
+ member name: "shrink_budget", type: "Integer",
670
+ meaning: "Candidate replays `hecks quality_control query sweep.run` may spend shrinking one surprised check; 0 turns shrinking off. `concurrency` is never shrunk: a race does not reproduce on demand."
671
+ member name: "yield_weight_seconds", type: "Integer",
672
+ meaning: "Seconds of staleness that one point of yield score is worth in `RotationPriority.pick`. Zero would make yield invisible, so pick a number that lets it matter."
673
+ member name: "yield_decay_percent", type: "Integer",
674
+ meaning: "Percent of the prior yield score kept at each release, so recent findings dominate. Applied by the caller: the mutation language cannot express \"decay, then add\" in `Release`."
675
+ member name: "rotation_stale_floor_seconds", type: "Integer",
676
+ meaning: "Once a waiting target is this stale, `RotationPriority.pick` takes it whatever its yield score, so a quiet target cannot starve."
677
+ member name: "persistence_parity_seed_cap", type: "Integer",
678
+ meaning: "Ceiling on `--seeds` for `--persistence-parity`, which pays for real Postgres per dispatch. `hecks quality_control query sweep.run` clamps down, never up, and prints a note."
679
+ member name: "concurrency_seed_cap", type: "Integer",
680
+ meaning: "Ceiling on `--seeds` for the concurrency mode, which needs two disposable schemas per seed."
681
+ member name: "adapter_parity_pairs", type: "Hash",
682
+ meaning: "The two adapters each adapter-parity mode compares, by mode name; a new pairing is a new entry. `adapter_parity_sqlite` needs no `database:`/`schema:` because sqlite has no disposable database."
683
+ member name: "modes", type: "Hash",
684
+ meaning: "Which comparison modes `hecks quality_control query sweep.run` may run: the enabled ones intersected with what `TargetCapabilities.infer(path)` finds in the target. `--modes a,b,c` overrides for one run. `wasm_front` and `adapter_parity_postgres` are off because nothing runs them; the others are built and off because their I/O cost across the rotation is unmeasured. `hecks quality_control query sweep.run` refuses to start when a mode outside `TargetCapabilities::RUNNABLE_MODES` is enabled."
685
+ member name: "role_draw_probability", type: "Numeric",
686
+ meaning: "How often a generated role-gated command step carries a drawn caller (`role:`/`actor_id:`); 0 turns the draw off."
687
+ member name: "dry_run_fraction", type: "Numeric",
688
+ meaning: "Fraction of generated command steps offered as a dry run instead of a real dispatch; 0 is off. Kept low: a dry run creates no state for later steps to act on."
689
+ member name: "generated_domains_per_tick", type: "Integer",
690
+ meaning: "How many generated domains one `hecks quality_control sweep.tick` checks after the rotation sweep; 0 skips the step."
691
+ member name: "generated_domains_rust", type: "Boolean",
692
+ meaning: "Whether generated domains are also compared against a compiled Rust binary built in a scratch copy."
693
+ member name: "generated_domain_seeds", type: "Integer",
694
+ meaning: "Generated sequences run per generated domain."
695
+ member name: "structural_refusal_boundary", type: "Array",
696
+ meaning: "Construct families a skipped query verb may belong to. A skip inside the list is logged Held, one outside it Surprised. Shrink-only: spec/fuzzing/rust_gap_manifest_spec.rb fails when a family here is declared by no committed manifest."
697
+ end
698
+
699
+ # What each retired `bin/` script became in hecks 3.0.0 (ADR 0080, section 7): one row per
700
+ # command or query that answers it, so a script that became several verbs has several rows.
701
+ # hecks project_tools_doc writes the launcher forms into the marked tables of docs/tools.md,
702
+ # rendering each form from the command's own arguments, so a form can neither keep an argument
703
+ # the command dropped nor miss one it gained; the table specs read the same rows.
704
+ #
705
+ # `section` is the part of docs/tools.md the row is listed under (Custodian, Deploy, Site,
706
+ # Codebase or QualityControl); Custodian and Codebase commands live in the Hecks chapter, the
707
+ # others in the chapter of their own name. `script` is `(new)` for a command no `bin/` script
708
+ # preceded. `aggregate` and `verb` name the command (or, in QualityControl, the query) the way
709
+ # the launcher spells it, `aggregate.verb`.
710
+ #
711
+ # The rest is only what the arguments cannot say: `passes` documents the flags a command hands
712
+ # through to the tool it wraps (its `arguments=` word), `note` is a sentence kept beside the
713
+ # form, and `form` replaces the rendered form when a launcher route spells it its own way.
714
+ value_object "RetiredScript" do
715
+ attribute :script, String
716
+ attribute :section, String
717
+ attribute :aggregate, String
718
+ attribute :verb, String
719
+ attribute :passes, String, optional: true
720
+ attribute :note, String, optional: true
721
+ attribute :form, String, optional: true
722
+
723
+ member script: "ir", section: "Custodian", aggregate: "Introspection", verb: "ir"
724
+ member script: "shape", section: "Custodian", aggregate: "Introspection", verb: "shape"
725
+ member script: "stores", section: "Custodian", aggregate: "Introspection", verb: "stores"
726
+ member script: "history", section: "Custodian", aggregate: "Introspection", verb: "history"
727
+ member script: "statements", section: "Custodian", aggregate: "Introspection", verb: "statements"
728
+ member script: "narrate", section: "Custodian", aggregate: "Introspection", verb: "narrate"
729
+ member script: "docs", section: "Custodian", aggregate: "Introspection", verb: "docs"
730
+ member script: "project_diagrams", section: "Custodian", aggregate: "Introspection", verb: "project_diagrams"
731
+ member script: "project_glossary", section: "Custodian", aggregate: "Introspection", verb: "glossary"
732
+ member script: "model_check", section: "Custodian", aggregate: "ModelCheckRun", verb: "model_check"
733
+ member script: "run", section: "Custodian", aggregate: "Operation", verb: "run", form: "hecks run [domain] <verb [name=value …] | script.json | - | '{\"steps\":[…]}'>"
734
+ member script: "project", section: "Custodian", aggregate: "Operation", verb: "refresh_projections"
735
+ member script: "behaviors", section: "Custodian", aggregate: "Operation", verb: "run_behaviors"
736
+ member script: "console", section: "Custodian", aggregate: "Operation", verb: "open_console"
737
+ member script: "follow", section: "Custodian", aggregate: "Operation", verb: "follow"
738
+ member script: "smoke_test", section: "Custodian", aggregate: "Operation", verb: "smoke_test"
739
+ member script: "smoke_http", section: "Custodian", aggregate: "Operation", verb: "smoke_http"
740
+ member script: "check_era", section: "Custodian", aggregate: "Host", verb: "check_era"
741
+ member script: "merge_tail", section: "Custodian", aggregate: "Era", verb: "merge_tail"
742
+ member script: "reattest_era", section: "Custodian", aggregate: "Era", verb: "reattest"
743
+ member script: "backfill_era_projections", section: "Custodian", aggregate: "Era", verb: "backfill_projections"
744
+ member script: "scaffold_translation", section: "Custodian", aggregate: "Era", verb: "scaffold_translation"
745
+ member script: "translation_audit", section: "Custodian", aggregate: "Era", verb: "audit_translation"
746
+ member script: "translation_audit", section: "Custodian", aggregate: "Era", verb: "approve_translation"
747
+ member script: "compact", section: "Custodian", aggregate: "Era", verb: "compact"
748
+ member script: "heki_compact", section: "Custodian", aggregate: "Era", verb: "compact_heki"
749
+ member script: "vendor_bluebook", section: "Custodian", aggregate: "Package", verb: "vendor", note: "waits: a refusal exits 1 with its reason on stderr"
750
+ member script: "project_cli", section: "Custodian", aggregate: "Door", verb: "project_cli"
751
+ member script: "hecks_mcp_door", section: "Custodian", aggregate: "Door", verb: "serve_mcp"
752
+ member script: "project_rust", section: "Custodian", aggregate: "Build", verb: "project_rust"
753
+ member script: "project_wasm", section: "Custodian", aggregate: "Build", verb: "build_wasm"
754
+ member script: "project_host", section: "Custodian", aggregate: "Build", verb: "build_host"
755
+ member script: "project_wasm_browser", section: "Custodian", aggregate: "Build", verb: "build_browser_wasm"
756
+ member script: "rust_coverage", section: "Custodian", aggregate: "Build", verb: "rust_coverage"
757
+ member script: "rust_coverage", section: "Custodian", aggregate: "Build", verb: "check_coverage_allowlist"
758
+ member script: "rust_conformance", section: "Custodian", aggregate: "Build", verb: "check_conformance"
759
+ member script: "rust_conformance_fuzz", section: "Custodian", aggregate: "Build", verb: "fuzz_conformance"
760
+ member script: "fuzz", section: "Custodian", aggregate: "FuzzRun", verb: "fuzz"
761
+ member script: "generate", section: "Custodian", aggregate: "FuzzRun", verb: "generate_sequence"
762
+ member script: "bench", section: "Custodian", aggregate: "FuzzRun", verb: "bench"
763
+ member script: "(new)", section: "Custodian", aggregate: "Era", verb: "hold_first"
764
+ member script: "project_deploy", section: "Deploy", aggregate: "Recipe", verb: "project"
765
+ member script: "lint_deploy_recipes", section: "Deploy", aggregate: "MakefileCheck", verb: "lint"
766
+ member script: "deploy_template_diff", section: "Deploy", aggregate: "TemplateComparison", verb: "diff"
767
+ member script: "project_oidc", section: "Deploy", aggregate: "OidcManifest", verb: "project_oidc"
768
+ member script: "project_tenant", section: "Deploy", aggregate: "Tenant", verb: "provision"
769
+ member script: "project_tenant", section: "Deploy", aggregate: "Tenant", verb: "reprovision"
770
+ member script: "(new)", section: "Site", aggregate: "SiteProjection", verb: "project_site"
771
+ member script: "project_model", section: "Codebase", aggregate: "LanguageRun", verb: "project_model"
772
+ member script: "project_vocabulary", section: "Codebase", aggregate: "LanguageRun", verb: "project_vocabulary"
773
+ member script: "project_rust_vocabulary", section: "Codebase", aggregate: "LanguageRun", verb: "project_rust_vocabulary"
774
+ member script: "project_refusal_wording", section: "Codebase", aggregate: "LanguageRun", verb: "project_refusal_wording"
775
+ member script: "project_reserved_names", section: "Codebase", aggregate: "LanguageRun", verb: "project_reserved_names"
776
+ member script: "project_parser_table", section: "Codebase", aggregate: "LanguageRun", verb: "project_parser_table"
777
+ member script: "project_bootstrap_table", section: "Codebase", aggregate: "LanguageRun", verb: "project_bootstrap_table"
778
+ member script: "project_field_hints", section: "Codebase", aggregate: "LanguageRun", verb: "project_field_hints"
779
+ member script: "expression_projection", section: "Codebase", aggregate: "LanguageRun", verb: "project_expression_tables"
780
+ member script: "reference", section: "Codebase", aggregate: "LanguageRun", verb: "project_reference"
781
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "word_status"
782
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "propose"
783
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "admit"
784
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "deprecate"
785
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "retire"
786
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "rename"
787
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "propose_argument"
788
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "admit_argument"
789
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "deprecate_argument"
790
+ member script: "evolve", section: "Codebase", aggregate: "LanguageRun", verb: "retire_argument"
791
+ member script: "project_kernel_capabilities", section: "Codebase", aggregate: "KernelRun", verb: "project_kernel_capabilities"
792
+ member script: "rust_kernel_coverage", section: "Codebase", aggregate: "KernelRun", verb: "measure_kernel_coverage"
793
+ member script: "check_engine_agreement", section: "Codebase", aggregate: "ConformanceRun", verb: "check_engine_agreement"
794
+ member script: "doc_coverage", section: "Codebase", aggregate: "ConformanceRun", verb: "measure_doc_coverage"
795
+ member script: "argument_gate_matrix", section: "Codebase", aggregate: "ConformanceRun", verb: "argument_gate_matrix"
796
+ member script: "regen_codegen_domains", section: "Codebase", aggregate: "RegenerationRun", verb: "regenerate_corpus"
797
+ member script: "standardize_comments", section: "Codebase", aggregate: "StyleRun", verb: "report_comments"
798
+ member script: "standardize_comments", section: "Codebase", aggregate: "StyleRun", verb: "check_comments"
799
+ member script: "standardize_comments", section: "Codebase", aggregate: "StyleRun", verb: "fix_comments"
800
+ member script: "standardize_comments", section: "Codebase", aggregate: "StyleRun", verb: "write_comment_baseline"
801
+ member script: "standardize_comments", section: "Codebase", aggregate: "StyleRun", verb: "check_comments_unchanged"
802
+ member script: "standardize_comments_rust", section: "Codebase", aggregate: "StyleRun", verb: "report_rust_comments"
803
+ member script: "standardize_comments_rust", section: "Codebase", aggregate: "StyleRun", verb: "check_rust_comments"
804
+ member script: "standardize_comments_rust", section: "Codebase", aggregate: "StyleRun", verb: "fix_rust_comments"
805
+ member script: "canonicalise", section: "Codebase", aggregate: "StyleRun", verb: "canonicalise"
806
+ member script: "codemod_hoist_local_givens", section: "Codebase", aggregate: "CodemodRun", verb: "hoist_local_givens"
807
+ member script: "codemod_implicit_append_fields", section: "Codebase", aggregate: "CodemodRun", verb: "drop_implicit_append_fields"
808
+ member script: "rspec_shard_files", section: "Codebase", aggregate: "TestSuiteRun", verb: "shard_specs"
809
+ member script: "rspec_io_parallel_files", section: "Codebase", aggregate: "TestSuiteRun", verb: "list_io_parallel_specs"
810
+ member script: "rspec_io_parallel_files", section: "Codebase", aggregate: "TestSuiteRun", verb: "write_io_parallel_spec_list"
811
+ member script: "refresh_rspec_runtime_baseline", section: "Codebase", aggregate: "TestSuiteRun", verb: "refresh_runtime_baseline"
812
+ member script: "spec_example", section: "Codebase", aggregate: "TestSuiteRun", verb: "run_spec_example"
813
+ member script: "stress_concurrency_specs", section: "Codebase", aggregate: "TestSuiteRun", verb: "stress_concurrency"
814
+ member script: "regenerate_persistence_legacy_fixtures", section: "Codebase", aggregate: "TestSuiteRun", verb: "regenerate_legacy_fixtures"
815
+ member script: "seed_semantics_corpus", section: "Codebase", aggregate: "TestSuiteRun", verb: "seed_semantics_corpus"
816
+ member script: "pattern-cases", section: "Codebase", aggregate: "TestSuiteRun", verb: "record_pattern_cases"
817
+ member script: "corpus", section: "Codebase", aggregate: "CorpusRun", verb: "rust_domains"
818
+ member script: "corpus", section: "Codebase", aggregate: "CorpusRun", verb: "regen_order"
819
+ member script: "corpus", section: "Codebase", aggregate: "CorpusRun", verb: "corpus_rust_coverage"
820
+ member script: "query_ir", section: "Codebase", aggregate: "CorpusRun", verb: "ir_constructs"
821
+ member script: "query_ir", section: "Codebase", aggregate: "CorpusRun", verb: "ir_duplicates"
822
+ member script: "query_ir", section: "Codebase", aggregate: "CorpusRun", verb: "ir_impact"
823
+ member script: "hecks_query_ir_mcp", section: "Codebase", aggregate: "CorpusRun", verb: "serve_query_ir_mcp"
824
+ member script: "present", section: "Codebase", aggregate: "CorpusRun", verb: "present"
825
+ member script: "release", section: "Codebase", aggregate: "PublishingRun", verb: "publish", note: "without --confirm, the old --dry-run"
826
+ member script: "release_gem", section: "Codebase", aggregate: "PublishingRun", verb: "publish_gem"
827
+ member script: "(new)", section: "Codebase", aggregate: "GateRun", verb: "gate"
828
+ member script: "qa_tick", section: "QualityControl", aggregate: "Sweep", verb: "tick"
829
+ member script: "qa_sweep", section: "QualityControl", aggregate: "Sweep", verb: "run", passes: "--all --seeds N --steps N --adversarial F --role-draw F --dry-run F --self-consistency BOOL --modes a,b --persistence-parity --no-parity"
830
+ member script: "qa_sweep", section: "QualityControl", aggregate: "Target", verb: "release"
831
+ member script: "qa_pr_check", section: "QualityControl", aggregate: "Clearance", verb: "check_pull_requests"
832
+ member script: "qa_open_pr", section: "QualityControl", aggregate: "Patch", verb: "open"
833
+ member script: "qa_open_pr", section: "QualityControl", aggregate: "Improvement", verb: "open"
834
+ member script: "qa_log_bug", section: "QualityControl", aggregate: "Bug", verb: "log"
835
+ member script: "qa_seed_angles", section: "QualityControl", aggregate: "Angle", verb: "seed"
836
+ member script: "qa_seed_targets", section: "QualityControl", aggregate: "Target", verb: "seed"
837
+ member script: "qa_generated_domains", section: "QualityControl", aggregate: "Target", verb: "check_generated_domains", passes: "--domains 3 --start N --forms a,b --seeds 5 --steps 25 --adversarial 0.3 --rust --shrink-budget 200 --domain-shrink-budget 40 --promote dir --name N --from-dials --blueprint F --source F"
838
+ member script: "qa_mine_combinations", section: "QualityControl", aggregate: "Target", verb: "mine_combinations", passes: "--candidates 3 --rust --seeds 5 --steps 25 --adversarial 0.3 --repair-rounds 1 --agent CMD --from DIR --against PATH --brief"
839
+ member script: "qa_domain_novelty", section: "QualityControl", aggregate: "Target", verb: "judge_novelty", passes: "--against PATH …"
840
+ member script: "qa_discover_external_domains", section: "QualityControl", aggregate: "Target", verb: "discover_external_domains", passes: "--projects-dir ~/Projects --max-depth 3 --known-path PATH …"
841
+ member script: "qa_postgres_migrate", section: "QualityControl", aggregate: "Sweep", verb: "migrate_ledger_from_heki", passes: "[aggregate …] --force", note: "a dry run without --force"
842
+ member script: "qa_postgres_role", section: "QualityControl", aggregate: "Sweep", verb: "create_ledger_role"
843
+ member script: "qa_concurrency_racer", section: "QualityControl", aggregate: "Sweep", verb: "race", note: "not user-facing; ProcessPool starts it"
844
+ end
845
+
633
846
  # Rust keywords (strict + reserved) a generated identifier must not
634
- # collide with. RustProjection::Projector::RUST_KEYWORDS reads this;
635
- # hecks project_reserved_names projects it into
636
- # rust/codegen/src/reserved_names.rs, so Ruby and Rust escape or refuse
637
- # the same words from one declaration instead of two hand-typed copies.
847
+ # collide with. ModelCheck reads this; hecks project_reserved_names
848
+ # projects it into rust/codegen/src/reserved_names.rs, so Ruby and Rust
849
+ # escape or refuse the same words from one declaration instead of two
850
+ # hand-typed copies.
638
851
  value_object "RustReservedWord" do
639
852
  attribute :word, String,
640
853
  one_of: ["as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false",
@@ -648,8 +861,8 @@ Hecks.bluebook "Bluebook" do
648
861
  # as a Cargo `[features]` key: every key rust/Cargo.toml already uses
649
862
  # outside `[features]` (`[package]`, `[lib]`, `[[bin]]`) plus `default`,
650
863
  # which Cargo reserves for the auto-enabled feature set. Read by
651
- # RustProjection::Projector::CARGO_RESERVED_DOMAIN_NAMES and projected by
652
- # hecks project_reserved_names, same as RustReservedWord above.
864
+ # Hecks::RustBuild::DomainName and projected by hecks
865
+ # project_reserved_names, same as RustReservedWord above.
653
866
  value_object "CargoReservedName" do
654
867
  attribute :name, String,
655
868
  one_of: ["name", "version", "edition", "path", "authors", "license", "description", "default",
@@ -83,7 +83,7 @@ Hecks.bluebook "Hecksagon" do
83
83
  member word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: ""
84
84
  member word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions"
85
85
  member word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments"
86
- # Deprecated spellings of `attaches`; they warn and are removed in 3.2.0.
86
+ # Deprecated spellings of `attaches`; they warn and are removed in 3.4.0.
87
87
  member word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
88
88
  member word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
89
89
  member word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", calls: "port_impl"
@@ -236,6 +236,11 @@
236
236
  "verb": "Bluebook::Query.Filter",
237
237
  "role": "Language"
238
238
  },
239
+ {
240
+ "scope": "bluebook:query.need",
241
+ "verb": "Bluebook::Query.Need",
242
+ "role": "Language"
243
+ },
239
244
  {
240
245
  "scope": "bluebook:query.option",
241
246
  "verb": "Bluebook::Query.Option",
@@ -58,7 +58,7 @@ module Hecks
58
58
  # @return [String] the `era` the host's `/version` document carries
59
59
  # @raise [Unreachable] if the request fails or the status is not `200`
60
60
  # @raise [BadResponse] if the body is not JSON or its `era` is not a non-empty string
61
- def fetch_era(url, timeout: 10)
61
+ def fetch_era(url, timeout:)
62
62
  fetch_version(url, timeout: timeout).fetch("era")
63
63
  end
64
64
 
@@ -70,7 +70,7 @@ module Hecks
70
70
  # host reports none)
71
71
  # @raise [Unreachable] if the request fails or the status is not `200`
72
72
  # @raise [BadResponse] if the body is not JSON or its `era` is not a non-empty string
73
- def fetch_version(url, timeout: 10)
73
+ def fetch_version(url, timeout:)
74
74
  response = get(version_url(url), timeout)
75
75
  raise Unreachable, "#{version_url(url)} answered #{response.code}" unless response.code == "200"
76
76
 
@@ -105,7 +105,7 @@ module Hecks
105
105
  # @raise [Errno::ENOENT] if the allow-list file does not exist
106
106
  # @raise [Unreachable] see `fetch_era`
107
107
  # @raise [BadResponse] see `fetch_era`
108
- def check(url, file, timeout: 10)
108
+ def check(url, file, timeout:)
109
109
  allowed = parse(File.read(file))
110
110
  verdict(fetch_era(url, timeout: timeout), allowed)
111
111
  end
@@ -0,0 +1,170 @@
1
+ require "shellwords"
2
+ require_relative "../scripts"
3
+
4
+ module Hecks
5
+ module Projections
6
+ module Deploy
7
+ module Box
8
+ # The hosting scripts an AWS box deploy ships beside its stacks when the world sets
9
+ # `hosting_scripts true`: the steps an operator runs once `make stacks` has created the box.
10
+ # Container names, repositories, parameters and stacks come from the resolved plan, never
11
+ # from this file.
12
+ module Hosting
13
+ module_function
14
+
15
+ # Adds the hosting scripts to a box file map when the world opts in.
16
+ #
17
+ # @param files [Hash{String => String}] the box target's own files, keyed by path
18
+ # @param plan [Settings::Plan] the resolved settings
19
+ # @param region [String] the validated AWS region
20
+ # @return [Hash{String => String}] `files` itself without the opt-in, else a copy with
21
+ # `hosting.mk`, `deploy-service.sh`, `smoke-after-deploy.sh` and `expected-era`
22
+ def extend_files(files, plan:, region:)
23
+ return files unless plan.hosting
24
+
25
+ files.merge(
26
+ "hosting.mk" => hosting_mk(plan),
27
+ "deploy-service.sh" => deploy_service_sh(plan, region),
28
+ "smoke-after-deploy.sh" => smoke_after_deploy_sh(plan, region),
29
+ "expected-era" => Scripts.expected_era(plan.hosting)
30
+ )
31
+ end
32
+
33
+ # @param plan [Settings::Plan] the resolved settings
34
+ # @return [String] the Makefile fragment the box `Makefile` includes
35
+ def hosting_mk(plan)
36
+ Box.template("hosting.mk.tmpl", "SERVICE" => plan.containers.first.name,
37
+ "URL" => plan.hosting.public_url.to_s)
38
+ end
39
+
40
+ # @param plan [Settings::Plan] the resolved settings
41
+ # @param region [String] the validated AWS region
42
+ # @return [String] `deploy-service.sh`
43
+ def deploy_service_sh(plan, region)
44
+ roll = plan.task_definition ? "deploy-service.taskdef-roll.part" : "deploy-service.services-roll.part"
45
+ Box.template("deploy-service.sh.tmpl",
46
+ "STACK" => plan.infra_name, "MODE_SUMMARY" => mode_summary(plan),
47
+ "SETTINGS" => deploy_constants(plan, region), "SERVICES" => services_block(plan),
48
+ "REPOSITORY_STEP" => repository_step(plan),
49
+ "ROLL_STEPS" => part(roll))
50
+ end
51
+
52
+ # @param plan [Settings::Plan] the resolved settings
53
+ # @param region [String] the validated AWS region
54
+ # @return [String] `smoke-after-deploy.sh`
55
+ def smoke_after_deploy_sh(plan, region)
56
+ hosting = plan.hosting
57
+ image_stack = hosting.stack
58
+ Box.template("smoke-after-deploy.sh.tmpl",
59
+ "STACK" => plan.infra_name, "SETTINGS" => smoke_constants(plan, region),
60
+ "SETTLE_CHECKS" => settle_checks(plan),
61
+ "IMAGE_STACK_WAIT" => image_stack ? ", stack #{image_stack}" : "")
62
+ end
63
+
64
+ # @param plan [Settings::Plan] the resolved settings
65
+ # @return [String] the sentence that says what the roll puts on the box
66
+ def mode_summary(plan)
67
+ if plan.task_definition
68
+ <<~TEXT.gsub(/^/, "# ").chomp
69
+ It then sets the stack's matching image-tag parameter (UsePreviousValue for every other
70
+ one, and it checks that only that one changed), reads the task definition the stack
71
+ registers, refuses to roll it unless it carries the pushed image, and rolls it with
72
+ deploy-box.sh.
73
+ TEXT
74
+ else
75
+ <<~TEXT.gsub(/^/, "# ").chomp
76
+ It then rolls the box with deploy-box.sh, naming the new tag for this container and the
77
+ tag the box runs now for every other one.
78
+ TEXT
79
+ end
80
+ end
81
+
82
+ # @param plan [Settings::Plan] the resolved settings
83
+ # @return [String] the comment lines that list what "settled" means
84
+ def settle_checks(plan)
85
+ stacks = plan.hosting.stack ? "the box stack and the #{plan.hosting.stack} stack are" : "the box stack is"
86
+ checks = ["#{stacks} CREATE_COMPLETE or UPDATE_COMPLETE, never rolled back or failed;",
87
+ "every container and the proxy of the box's Compose project is up and has stayed up;"]
88
+ if plan.task_definition
89
+ checks << "each container runs the image of the task definition it was rolled from (the latest " \
90
+ "#{plan.task_definition} unless TASKDEF says otherwise)."
91
+ end
92
+ checks.map { |line| "# - #{line}" }.join("\n")
93
+ end
94
+
95
+ def deploy_constants(plan, region)
96
+ lines = { "REGION" => region }
97
+ if plan.task_definition
98
+ lines["FAMILY"] = plan.task_definition
99
+ lines["IMAGE_STACK"] = plan.hosting.stack
100
+ else
101
+ lines["BOX_STACK"] = plan.box_stack
102
+ lines["BOX_DIR"] = "/opt/#{plan.infra_name}"
103
+ end
104
+ constants(lines)
105
+ end
106
+
107
+ def smoke_constants(plan, region)
108
+ hosting = plan.hosting
109
+ watched = [plan.box_stack, hosting.stack].compact
110
+ constants(
111
+ "REGION" => region, "BOX_STACK" => plan.box_stack, "BOX_DIR" => "/opt/#{plan.infra_name}",
112
+ "WATCHED_STACKS" => watched.join(" "),
113
+ "EXPECTED_SERVICES" => (plan.containers.map(&:name) + ["caddy"]).join(" ")
114
+ ).concat("\n#{smoke_defaults(plan)}")
115
+ end
116
+
117
+ def smoke_defaults(plan)
118
+ hosting = plan.hosting
119
+ [
120
+ "TASK_DEFINITION=\"${TASKDEF:-#{plan.task_definition}}\"",
121
+ "REPO=\"${REPO:-#{hosting.smoke_repo}}\"",
122
+ "WORKFLOW=\"${WORKFLOW:-#{hosting.smoke_workflow}}\"",
123
+ "SMOKE_REF=\"${SMOKE_REF:-#{hosting.smoke_ref}}\""
124
+ ].join("\n")
125
+ end
126
+
127
+ def constants(values)
128
+ values.map { |name, value| "#{name}=#{Shellwords.escape(value)}" }.join("\n")
129
+ end
130
+
131
+ # One case arm per container that sets the repository and image-tag parameter to use, and
132
+ # the list of names the script accepts.
133
+ def services_block(plan)
134
+ arms = plan.containers.map do |container|
135
+ setting =
136
+ if plan.task_definition
137
+ "CFN_PARAM_KEY=#{Shellwords.escape(container.tag_parameter)}"
138
+ else
139
+ "ECR_REPOSITORY=#{Shellwords.escape(container.repository)}"
140
+ end
141
+ " #{Shellwords.escape(container.name)}) #{setting} ;;"
142
+ end
143
+ <<~BASH.chomp
144
+ SERVICES='#{plan.containers.map(&:name).join(' ')}'
145
+
146
+ # Sets the container's settings for one service, or exits.
147
+ resolve_service() {
148
+ case "$1" in
149
+ #{arms.join("\n")}
150
+ *) echo "unknown service '$1', must be one of: ${SERVICES}" >&2; exit 1 ;;
151
+ esac
152
+ }
153
+ BASH
154
+ end
155
+
156
+ def repository_step(plan)
157
+ plan.task_definition ? "#{part('deploy-service.taskdef.part')}\n\n" : ""
158
+ end
159
+
160
+ def part(file)
161
+ file ? File.read(File.join(Box::TEMPLATE_DIR, file)).chomp : ""
162
+ end
163
+
164
+ private_class_method :deploy_constants, :smoke_constants, :smoke_defaults, :constants,
165
+ :services_block, :part, :repository_step, :settle_checks
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,123 @@
1
+ require_relative "../scripts/settings"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Deploy
6
+ module Box
7
+ # The hosting words of a `deployed_to("AwsBox")` block: `hosting_scripts true` and the
8
+ # settings the generated `deploy-service.sh`, `smoke-after-deploy.sh` and `expected-era`
9
+ # read. Every value is matched against a conservative pattern before a script splices it in.
10
+ module HostingSettings
11
+ PARAMETER = /\A[A-Za-z][A-Za-z0-9]{0,254}\z/
12
+ STACK = /\A[a-zA-Z][a-zA-Z0-9-]{0,127}\z/
13
+ PUBLIC_URL = %r{\Ahttps?://[^\s'"$`\\]+\z}
14
+ # The words that only mean something to the hosting scripts.
15
+ WORDS = %i[hosting_stack smoke_repo smoke_workflow smoke_ref expected_eras public_url].freeze
16
+
17
+ # The checked hosting settings.
18
+ #
19
+ # @!attribute [r] stack [String, nil] the CloudFormation stack whose image-tag parameters
20
+ # feed the task definition; nil without a `task_definition`
21
+ # @!attribute [r] smoke_repo [String, nil] the GitHub repository holding the workflow
22
+ # @!attribute [r] smoke_workflow [String] the smoke workflow's file name
23
+ # @!attribute [r] smoke_ref [String] the ref the smoke workflow is dispatched on
24
+ # @!attribute [r] expected_eras [Array<String>] the eras `expected-era` lists
25
+ # @!attribute [r] public_url [String, nil] the live site, for `make check-era`
26
+ Hosting = Struct.new(:stack, :smoke_repo, :smoke_workflow, :smoke_ref, :expected_eras,
27
+ :public_url, keyword_init: true)
28
+
29
+ module_function
30
+
31
+ # @param settings [Hash{Symbol => Object}] the world's `AwsBox` settings
32
+ # @param task_definition [String, nil] the declared task definition family
33
+ # @return [Hosting, nil] the checked settings, or nil when the world has not opted in
34
+ # @raise [ArgumentError] when a hosting word is invalid, missing or has nothing to act on
35
+ def read(settings, task_definition)
36
+ flag = settings.fetch(:hosting_scripts, false)
37
+ raise ArgumentError, "hosting_scripts: #{flag.inspect} must be true or false" unless [true, false].include?(flag)
38
+
39
+ unless flag
40
+ stray = WORDS.select { |word| settings.key?(word) }
41
+ raise ArgumentError, "#{stray.join(', ')} only apply with hosting_scripts true" unless stray.empty?
42
+
43
+ return nil
44
+ end
45
+
46
+ Hosting.new(stack: read_stack(settings, task_definition), **read_smoke(settings),
47
+ expected_eras: eras(settings), public_url: optional(settings, :public_url, PUBLIC_URL))
48
+ end
49
+
50
+ # With a task definition, the stack that registers its revisions is where a new image tag
51
+ # goes, so it has to be named. Without one the box is rolled from `services.json` and no
52
+ # stack holds a tag.
53
+ def read_stack(settings, task_definition)
54
+ stack = settings[:hosting_stack]
55
+ if task_definition.nil?
56
+ raise ArgumentError, "hosting_stack names the stack behind a task_definition; this world has none" if stack
57
+
58
+ return nil
59
+ end
60
+ raise ArgumentError, missing_stack(task_definition) if stack.nil?
61
+
62
+ check(:hosting_stack, stack, STACK)
63
+ end
64
+
65
+ def read_smoke(settings)
66
+ workflow = settings[:smoke_workflow]
67
+ raise ArgumentError, missing_workflow if workflow.nil?
68
+
69
+ {
70
+ smoke_workflow: check(:smoke_workflow, workflow, Scripts::Settings::WORKFLOW),
71
+ smoke_repo: optional(settings, :smoke_repo, Scripts::Settings::GITHUB_REPO),
72
+ smoke_ref: check(:smoke_ref, settings.fetch(:smoke_ref, "main"), Scripts::Settings::REF)
73
+ }
74
+ end
75
+
76
+ def eras(settings)
77
+ Array(settings.fetch(:expected_eras, [])).map { |era| check(:expected_eras, era.to_s, Scripts::Settings::ERA) }
78
+ end
79
+
80
+ def optional(settings, key, pattern)
81
+ value = settings[key]
82
+ value.nil? ? nil : check(key, value, pattern)
83
+ end
84
+
85
+ def check(key, value, pattern)
86
+ return value if value.is_a?(String) && value.match?(pattern)
87
+
88
+ raise ArgumentError, "#{key}: #{value.inspect} does not match #{pattern.inspect}"
89
+ end
90
+
91
+ def missing_workflow
92
+ <<~MSG
93
+ deployed_to("AwsBox") enables hosting_scripts but names no smoke_workflow. Name the GitHub
94
+ workflow file the post-deploy smoke dispatches, e.g.:
95
+
96
+ deployed_to("AwsBox") do
97
+ ...
98
+ hosting_scripts true
99
+ smoke_workflow "smoke-prod.yml"
100
+ end
101
+
102
+ A deploy that ends without a smoke of the live site cannot tell a roll that took from one
103
+ that only looked live.
104
+ MSG
105
+ end
106
+
107
+ def missing_stack(family)
108
+ <<~MSG
109
+ deployed_to("AwsBox") enables hosting_scripts with task_definition #{family.inspect} but names no
110
+ hosting_stack. Name the CloudFormation stack whose TaskDefinition resource reads an image-tag
111
+ parameter per container (`<Name>ImageTag` unless a container sets `tag_parameter`), e.g.:
112
+
113
+ hosting_stack "my-platform"
114
+
115
+ deploy-service.sh sets that parameter to the tag it pushed, so the stack, not a hand-registered
116
+ revision, is the source of truth for what runs.
117
+ MSG
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end