hecks 3.11.0 → 3.13.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/script_runner.adapter +3 -0
  3. data/lib/hecks/adapters/driven/script_runner.rb +93 -0
  4. data/lib/hecks/adapters/driven.rb +1 -0
  5. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
  6. data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
  7. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  8. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  9. data/lib/hecks/bluebook/assembly/marks.rb +3 -15
  10. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  11. data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
  12. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  13. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +16 -12
  14. data/lib/hecks/bluebook/meta_validator.rb +28 -21
  15. data/lib/hecks/bluebook.rb +1 -0
  16. data/lib/hecks/deploy/bluebook/deploy.bluebook +17 -3
  17. data/lib/hecks/framework/bluebook/change.behaviors +30 -0
  18. data/lib/hecks/framework/bluebook/change.bluebook +162 -0
  19. data/lib/hecks/framework/bluebook/change.hecksagon +21 -0
  20. data/lib/hecks/framework/change.oidc.json +34 -0
  21. data/lib/hecks/framework/console_settings.oidc.json +103 -0
  22. data/lib/hecks/framework/governance.oidc.json +28 -0
  23. data/lib/hecks/framework/identity.oidc.json +18 -0
  24. data/lib/hecks/framework/privacy.oidc.json +30 -0
  25. data/lib/hecks/gate/stages.yml +35 -0
  26. data/lib/hecks/grammar/translation.oidc.json +48 -0
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +12 -1
  28. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -1
  29. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
  30. data/lib/hecks/hecks/hecks.world +3 -0
  31. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  32. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  33. data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
  34. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  35. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  36. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  37. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  38. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  39. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  40. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  41. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  42. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  43. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  44. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  45. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  46. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  47. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
  48. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  49. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  50. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  51. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  52. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  53. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  54. data/lib/hecks/projections/deploy/box.rb +15 -3
  55. data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +12 -1
  56. data/lib/hecks/projections/site/admin_cms/templates/membership.rb +10 -7
  57. data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
  58. data/lib/hecks/projections/site/edge/checks.rb +2 -0
  59. data/lib/hecks/projections/site/edge/function_checks.rb +104 -0
  60. data/lib/hecks/projections/site/edge/function_code.rb +86 -0
  61. data/lib/hecks/projections/site/edge/redirect_host.js.tmpl +65 -0
  62. data/lib/hecks/projections/site/edge.rb +35 -9
  63. data/lib/hecks/projections/site/site_cdn.rb +21 -7
  64. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  65. data/lib/hecks/projections/site/table/row_builder.rb +12 -2
  66. data/lib/hecks/projections/site/table.rb +4 -2
  67. data/lib/hecks/projector/cli_audience.rb +20 -0
  68. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  69. data/lib/hecks/rust_build/host.rb +11 -4
  70. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  71. data/lib/hecks/site/bluebook/site.bluebook +16 -1
  72. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  73. data/lib/hecks/tools/ci_gates.rb +20 -1
  74. data/lib/hecks/tools/lanes/files.rb +8 -0
  75. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  76. data/lib/hecks/tools/lanes.rb +1 -0
  77. data/lib/hecks/tools/oidc_manifests.rb +44 -6
  78. data/lib/hecks/tools/release_only.rb +111 -0
  79. data/lib/hecks/tools/site_routes/templates.rb +13 -9
  80. data/lib/hecks/version.rb +1 -1
  81. data/lib/hecks/vocabulary.rb +15 -1
  82. data/prebuilt/2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal +0 -0
  83. data/prebuilt/58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal +0 -0
  84. data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json} +1 -1
  85. data/rust/host/HECKS_RELEASE +1 -1
  86. data/rust/host/src/auth.rs +58 -0
  87. data/rust/host/src/extension.rs +13 -1
  88. data/rust/host/src/lib.rs +1 -0
  89. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  90. data/rust/host/src/platform_signin/config.rs +512 -0
  91. data/rust/host/src/platform_signin/cookie.rs +237 -0
  92. data/rust/host/src/platform_signin/extension.rs +190 -0
  93. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  94. data/rust/host/src/platform_signin/handler.rs +251 -0
  95. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  96. data/rust/host/src/platform_signin/jti.rs +196 -0
  97. data/rust/host/src/platform_signin/jwks.rs +542 -0
  98. data/rust/host/src/platform_signin/jws.rs +249 -0
  99. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  100. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  101. data/rust/host/src/platform_signin/mod.rs +75 -0
  102. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  103. data/rust/host/src/platform_signin/pages.rs +204 -0
  104. data/rust/host/src/platform_signin/query.rs +233 -0
  105. data/rust/host/src/platform_signin/session.rs +151 -0
  106. data/rust/host/src/platform_signin/settings.rs +385 -0
  107. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  108. data/rust/host/src/platform_signin/test_support.rs +235 -0
  109. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  110. data/rust/host/src/platform_signin/verify.rs +604 -0
  111. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  112. data/rust/host/src/web.rs +50 -6
  113. data/rust/parser/src/parse/value_object.rs +22 -10
  114. metadata +59 -7
  115. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  116. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  117. /data/lib/hecks/framework/{oidc.json → compliance.oidc.json} +0 -0
  118. /data/lib/hecks/grammar/{oidc.json → expression.oidc.json} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 130184b20d584f87b5ede5d45364b213c07677a3330fa198bee6680077ef1899
4
- data.tar.gz: f60d730c4d1fc428703bdcc3dcd63f07ea10b77386e0eab4dcab12180e04cac4
3
+ metadata.gz: e1a1eacc3bae3f940928e23269c95e0306f291ec5a2b6d62268e254f12f1902e
4
+ data.tar.gz: e4168d9e617dbad8912a65dbe10be8a3a32a6a08b18fd84542a05fa9803aeaef
5
5
  SHA512:
6
- metadata.gz: 5afb2bcf079a1024b02260e7e428cbb5dbb7661d4df86e093c7813879b0617aae35c63bf25c4fac8025542e5ed05c0cd199eb1046b8ce52555e5ca1717c9662a
7
- data.tar.gz: e5cc44483d1cf6eff9c9fcbc0beef4b0c42b70c46ce0a9d50c26b4f4012604dd17200104036833e062bdb3921232c0938d58841c779c7a952bc0562e919b78a3
6
+ metadata.gz: a49c8992b247138d92232d00123dd73986be81040bb1356fc912f97905644bbb35a38af431a1d81475b9ce07c644ab8c2aab3370c3fabaf685c0f8a93a16bd70
7
+ data.tar.gz: 48ccea615a818989da2a01babb5c9d344a5a7c94fbe2eea47301a1437f29aa671134011a626f317c128bcaf1aaa912a21f056e30c8b133643123df129aa7a470
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "ScriptRunner" do
2
+ port "Script"
3
+ end
@@ -0,0 +1,93 @@
1
+ require "digest"
2
+ require "json"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ # The `Script` port's adapter for a `Change`: it runs the scripts registered with it, in this
7
+ # process. A script is a name, a `plan` that says what it would do without doing it, and an
8
+ # `apply` that does it.
9
+ #
10
+ # `examine` plans a change and answers the plan with a digest of it. `perform` plans again and
11
+ # carries the change out only when the plan still has the digest that was confirmed, so what
12
+ # runs is what the person saw. A script that cannot be found, or whose plan or apply raises,
13
+ # refuses the change with its message.
14
+ class ScriptRunner
15
+ # A script: what it would do, and doing it. Both take the arguments as a Hash of String keys.
16
+ Script = Struct.new(:plan, :apply, keyword_init: true)
17
+
18
+ # Why a change was refused: an unknown script, a plan that moved, or the script's own failure.
19
+ class Refused < StandardError; end
20
+
21
+ class << self
22
+ # @return [Hash{String => Script}] the scripts this process can run, by name
23
+ def scripts = @scripts ||= {}
24
+
25
+ # @param name [String] the name a change asks for
26
+ # @param plan [#call] takes the arguments, returns the text of what the script would do
27
+ # @param apply [#call] takes the arguments, does it, returns the text of what happened
28
+ # @return [Script] the registered script
29
+ def register(name, plan:, apply:) = scripts[name.to_s] = Script.new(plan: plan, apply: apply)
30
+
31
+ # Forgets every registered script.
32
+ #
33
+ # @return [Hash] the empty registry
34
+ def reset! = @scripts = {}
35
+ end
36
+
37
+ # Accepts the arguments every driven adapter is built with and keeps none of them.
38
+ #
39
+ # @param aggregate [Object, nil] unused
40
+ # @param settings [Hash] unused
41
+ # @param root [String, nil] unused
42
+ def initialize(aggregate: nil, settings: {}, root: nil); end
43
+
44
+ # Plans the change.
45
+ #
46
+ # @param held [Hash] the record: `script` and `arguments`
47
+ # @return [Hash{Symbol => Hash}] `plan:` what the script would do, and `digest:` its
48
+ # fingerprint
49
+ # @raise [Refused] when the script is unknown or its plan raises
50
+ def examine(**held)
51
+ name, args = request(held)
52
+ text = planned(name, args)
53
+ { plan: { value: text }, digest: { value: digest(name, args, text) } }
54
+ end
55
+
56
+ # Carries out a confirmed change if its plan has not moved.
57
+ #
58
+ # @param held [Hash] the record: `script`, `arguments` and `planned_digest`
59
+ # @return [Hash{Symbol => Hash}] `report:` what the script reported
60
+ # @raise [Refused] when the script is unknown, the plan has moved, or the script raises
61
+ def perform(**held)
62
+ name, args = request(held)
63
+ current = digest(name, args, planned(name, args))
64
+ raise Refused, "the plan moved since it was confirmed; plan it again" unless current == plain(held[:planned_digest])
65
+
66
+ { report: { value: run(name, :apply, args).to_s } }
67
+ end
68
+
69
+ private
70
+
71
+ def request(held) = [plain(held[:script]), arguments(held[:arguments])]
72
+
73
+ def planned(name, args) = run(name, :plan, args).to_s
74
+
75
+ def run(name, step, args)
76
+ script = self.class.scripts.fetch(name) { raise Refused, "no script is registered as #{name.inspect}" }
77
+ script.public_send(step).call(args)
78
+ rescue Refused
79
+ raise
80
+ rescue StandardError => e
81
+ raise Refused, "#{name} #{step} failed: #{e.message}"
82
+ end
83
+
84
+ # The digest stands for the script, its arguments and its plan, so none of them can change
85
+ # between confirming and applying.
86
+ def digest(name, args, text) = Digest::SHA256.hexdigest(JSON.generate([name, args.sort.to_h, text]))
87
+
88
+ def arguments(list) = Array(list).to_h { |pair| [plain(pair[:key]), plain(pair[:value])] }
89
+
90
+ def plain(cell) = cell.is_a?(Hash) && cell.key?(:value) ? cell[:value] : cell
91
+ end
92
+ end
93
+ end
@@ -27,3 +27,4 @@ require_relative "driven/governance_authorization"
27
27
  require_relative "driven/identity_registry"
28
28
  require_relative "driven/google_authentication"
29
29
  require_relative "driven/claude_code"
30
+ require_relative "driven/script_runner"
@@ -82,8 +82,9 @@ module Hecks
82
82
  # with the first sentence of what it is for.
83
83
  #
84
84
  # @return [Hash{Symbol => Object}] `:hide` and `:chapters` (pairs of word and summary)
85
- def audience(runtime, launcher, maintainer: LauncherOptions.maintainer?)
86
- shown = LauncherOptions.audience(launcher, maintainer)
85
+ def audience(runtime, launcher, maintainer: LauncherOptions.maintainer?,
86
+ newcomer: !maintainer && Newcomer.applies?)
87
+ shown = (newcomer && Newcomer.audience(launcher)) || LauncherOptions.audience(launcher, maintainer)
87
88
  return shown unless shown[:chapters]
88
89
 
89
90
  shown.merge(chapters: chapter_summaries(runtime, shown[:chapters]))
@@ -102,7 +103,9 @@ module Hecks
102
103
 
103
104
  # Names who the help is for, so the help remembered for one is not given to the other.
104
105
  def audience_key
105
- LauncherOptions.maintainer? ? "maintainer" : "project"
106
+ return "maintainer" if LauncherOptions.maintainer?
107
+
108
+ Newcomer.applies? ? "newcomer" : "project"
106
109
  end
107
110
 
108
111
  # Reads the entry words of a line: whether it asks a query, the bare name, and the words
@@ -3,6 +3,7 @@ require_relative "cli"
3
3
  require_relative "command_request"
4
4
  require_relative "json"
5
5
  require_relative "launcher_options"
6
+ require_relative "newcomer"
6
7
  require_relative "../../projector"
7
8
  require_relative "../../ports/clock"
8
9
  require_relative "cli_runner/answers"
@@ -0,0 +1,50 @@
1
+ require_relative "launcher_options"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ module Driving
6
+ # The launcher's help for someone starting out: a folder with no bluebook in it has no use yet
7
+ # for the commands that act on a domain, so the help lists only the `newcomer` commands the
8
+ # chapter's `launcher` setting names (`hecks --all` lists the rest).
9
+ module Newcomer
10
+ # How deep under a directory a bluebook may sit and still make it a project:
11
+ # `Hecks.bluebook` beside it, or `bluebook/pizzas.bluebook` one folder down, as the
12
+ # examples keep theirs.
13
+ PROJECT_GLOBS = ["*.bluebook", "*/*.bluebook", "*/*/*.bluebook"].freeze
14
+
15
+ module_function
16
+
17
+ # Whether the person is in a folder with no bluebook in it, outside a hecks checkout.
18
+ #
19
+ # @param dir [String] where the command line was typed
20
+ # @param env [#[]] the environment
21
+ # @return [Boolean] true when no `.bluebook` stands in `dir` or up to two folders below it
22
+ def applies?(dir = Dir.pwd, env = ENV)
23
+ return false if LauncherOptions.maintainer?(dir, env)
24
+
25
+ PROJECT_GLOBS.none? { |pattern| bluebook_under?(dir, pattern) }
26
+ end
27
+
28
+ # The help options a newcomer gets: only the setting's `newcomer` commands, and no chapters.
29
+ #
30
+ # @param settings [Hash, nil] the chapter's `launcher` setting
31
+ # @return [Hash{Symbol => Object}, nil] `:only` and `:chapters`, or nil when the chapter
32
+ # names no newcomer commands
33
+ def audience(settings)
34
+ return unless settings && settings[:newcomer]
35
+
36
+ { only: Array(settings[:newcomer]).map(&:to_s), chapters: [] }
37
+ end
38
+
39
+ # Whether `pattern` finds a bluebook under `dir`; a folder the person may not read holds
40
+ # none.
41
+ # @api private
42
+ def bluebook_under?(dir, pattern)
43
+ Dir.glob(File.join(File.expand_path(dir), pattern)).any?
44
+ rescue SystemCallError
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -61,7 +61,7 @@ module Hecks
61
61
  "Member" => Contract.new(
62
62
  holder: nil, make: nil,
63
63
  fields: {},
64
- rows: { pairs: :pair_rows },
64
+ rows: { pairs: :member_pair_rows },
65
65
  derived: { position: :walk, pairs: [:folded, %i[members], nil] }
66
66
  )
67
67
  }.freeze
@@ -35,10 +35,10 @@ module Hecks
35
35
  # One identity part, as the String path `identity_paths` splits.
36
36
  def identity_path(part) = part[:value].to_s
37
37
 
38
- # A member's fields, an open map, so the pairs arrive as a list.
39
- # Values are unmarked because `ValueObject#to_h` spells them with `to_s`.
38
+ # A member's fields, an open map, so the pairs arrive as a list. Each value is already
39
+ # typed: `Reconstruction` reads a stored cell with `Scalars.unmark` before it gets here.
40
40
  def member(pairs)
41
- pairs.to_h { |key, value| [key.to_sym, unmark_scalar(value)] }
41
+ pairs.to_h { |key, value| [key.to_sym, value] }
42
42
  end
43
43
 
44
44
  # A read model's gathered head; `as` is a Symbol because `ReadModel#to_h` spells it `to_s`.
@@ -49,18 +49,6 @@ module Hecks
49
49
  # Matches the builder's native `{field: :symbol}` shape.
50
50
  def group_by_field(row) = { field: row[:field].to_sym }
51
51
 
52
- # Reads a member's scalar back from text. Unlike `read`, a bare word stays a
53
- # String, because a closed set admits words far more often than symbols.
54
- def unmark_scalar(value)
55
- text = value.to_s
56
- return true if text == "true"
57
- return false if text == "false"
58
- return text.to_i if text.match?(/\A-?\d+\z/)
59
- return text.to_f if text.match?(/\A-?\d+\.\d+\z/)
60
-
61
- text
62
- end
63
-
64
52
  # A saga's argument bindings; Literal marks a Symbol with a leading colon.
65
53
  def bindings(with) = Array(with).to_h { |key, value| [key.to_sym, read(value)] }
66
54
 
@@ -0,0 +1,39 @@
1
+ module Hecks
2
+ module Bluebook
3
+ class Assembly
4
+ # How a `member` cell is spelled as stored text, and read back. The meta-domain holds a cell
5
+ # as text, so a quoted `"true"` and a bare `true` need different spellings: a boolean or
6
+ # number is its own spelling, and a String that would read back as one, or as quoted text,
7
+ # is quoted so it stays a String. The Rust parser reads a cell the same way (`cell_value`
8
+ # in `parse/value_object.rs`).
9
+ module Scalars
10
+ module_function
11
+
12
+ # @param value [Object] a cell as the builder holds it
13
+ # @return [Object] `value` itself unless it is a String that reads back as another type, in
14
+ # which case its quoted spelling
15
+ def mark(value)
16
+ return value unless value.is_a?(String)
17
+
18
+ unmark(value) == value ? value : Literal.quote(value)
19
+ end
20
+
21
+ # Unlike `Literal.read`, a bare word stays a String, because a closed set admits words far
22
+ # more often than symbols; quoted text is a String whatever it spells.
23
+ #
24
+ # @param value [#to_s] a cell's stored text
25
+ # @return [Object] the String, boolean or number the text spells
26
+ def unmark(value)
27
+ text = value.to_s
28
+ return Literal.unquote(text) if Literal.quoted?(text)
29
+ return true if text == "true"
30
+ return false if text == "false"
31
+ return text.to_i if text.match?(/\A-?\d+\z/)
32
+ return text.to_f if text.match?(/\A-?\d+\.\d+\z/)
33
+
34
+ text
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -70,6 +70,12 @@ module Hecks
70
70
  Array(map&.to_h).map { |key, value| { key: key, value: value } }
71
71
  end
72
72
 
73
+ # A member's cells, each spelled so a quoted `"true"` stays a String and a bare `true` a
74
+ # boolean.
75
+ def member_pair_rows(map)
76
+ Array(map&.to_h).map { |key, value| { key: key, value: Assembly::Scalars.mark(value) } }
77
+ end
78
+
73
79
  # Flattens options via `extra_options_to_h` — a new option needs no
74
80
  # change here. `filters: true` reads wheres/order_by/limit live off
75
81
  # the node, not `to_h`, so wire-format changes there don't affect it.
@@ -41,11 +41,14 @@ module Hecks
41
41
  # dispatched state, one member at a time, not behind a `DeclaredIn` query.
42
42
  def members_row(row) = members_of(row)
43
43
 
44
- # The key is stringified, never the value: a pair's value keeps whatever
45
- # native Ruby type the source line actually wrote.
44
+ # The key is stringified, never the value: a pair's value is read back from its text by
45
+ # `Assembly::Scalars`, so a quoted cell returns a String and `true`, `false` and numbers
46
+ # return as the builder holds them.
46
47
  def members_of(value_object_row)
47
48
  Array(value_object_row[:members]).map do |member|
48
- Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])] }
49
+ Array(member[:pairs]).map do |pair|
50
+ [text(pair[:key]).to_s, Assembly::Scalars.unmark(text(pair[:value]))]
51
+ end
49
52
  end
50
53
  end
51
54
 
@@ -37,19 +37,23 @@ module Hecks
37
37
  end
38
38
 
39
39
  # Dispatches every seed row into a fresh "Bluebook" instance and
40
- # reads the result back.
40
+ # reads the result back. The table is memoized for the process, so it is built with no
41
+ # ambient caller: an enclosing `Hecks.as_caller` would have the language's own
42
+ # `Language`-role commands refused.
41
43
  def boot
42
- bluebook = MetaValidator.grammar_registry.bluebook("Bluebook")
43
- # `fresh_runtime`, not a new `Runtime::Registry` — the grammar
44
- # registry already has "Bluebook" registered and its adapter
45
- # ports already loaded.
46
- runtime = MetaValidator.fresh_runtime
47
-
48
- declare_syntax(runtime, bluebook)
49
- admit_keywords(runtime, bluebook)
50
- admit_arguments(runtime, bluebook)
51
-
52
- read_back(runtime, bluebook)
44
+ Runtime::Caller.without do
45
+ bluebook = MetaValidator.grammar_registry.bluebook("Bluebook")
46
+ # `fresh_runtime`, not a new `Runtime::Registry` — the grammar
47
+ # registry already has "Bluebook" registered and its adapter
48
+ # ports already loaded.
49
+ runtime = MetaValidator.fresh_runtime
50
+
51
+ declare_syntax(runtime, bluebook)
52
+ admit_keywords(runtime, bluebook)
53
+ admit_arguments(runtime, bluebook)
54
+
55
+ read_back(runtime, bluebook)
56
+ end
53
57
  end
54
58
 
55
59
  # An Integer stays an Integer — `position` is `Position`-typed, so
@@ -168,32 +168,39 @@ module Hecks
168
168
  end
169
169
 
170
170
  # The booted, self-judged language registry every ordinary bluebook is
171
- # judged against — built once per process and memoized, with shadow parsing off: a caller
172
- # inside `while_shadow_parsing` that is first to ask would otherwise memoize a grammar
173
- # parsed in that mode, or fail to load it.
171
+ # judged against — built once per process and memoized, with shadow parsing off and no
172
+ # ambient caller bound. A caller inside `while_shadow_parsing` that is first to ask would
173
+ # otherwise memoize a grammar parsed in that mode, or fail to load it; one inside
174
+ # `Hecks.as_caller` would have the language's own `Language`-role commands refused, and the
175
+ # grammar would be memoized without the chapters those refusals dropped.
174
176
  #
175
177
  # @return [Runtime::Registry] the booted, fixpoint-judged language
176
178
  # registry
177
179
  def self.grammar_registry
178
- @grammar_registry ||= while_not_shadow_parsing do
179
- registry = load_grammar_into(Runtime::Registry.new)
180
- # Assigned before the fixpoint judge below, since judging re-enters
181
- # grammar_registry (via fresh_runtime/Plan.for) and a bare `||=`
182
- # would still be nil mid-evaluation, recursing forever.
183
- @grammar_registry = registry
184
- # The language now judges itself; every bluebook from here on is
185
- # judged by that assembled graph, not the raw bootstrap one. Must
186
- # run after load_grammar_into's ensure clears @bootstrapping.
187
- while_forcing_fixpoint do
188
- LANGUAGE_CHAPTERS.each { |name| registry.add_bluebook(call(registry.bluebook(name))) }
189
- load_attached_grammar_into(registry)
190
- end
191
- # Keyed on this registry's object identity, not a bare boolean, so
192
- # a manual reset can never leave a stale "ready" from the previous
193
- # cycle.
194
- @grammar_ready_for = registry.object_id
195
- registry
180
+ @grammar_registry ||= while_not_shadow_parsing { Runtime::Caller.without { build_grammar_registry } }
181
+ end
182
+
183
+ # The one-time build behind `grammar_registry`.
184
+ #
185
+ # @return [Runtime::Registry] the booted, fixpoint-judged language registry
186
+ def self.build_grammar_registry
187
+ registry = load_grammar_into(Runtime::Registry.new)
188
+ # Assigned before the fixpoint judge below, since judging re-enters
189
+ # grammar_registry (via fresh_runtime/Plan.for) and a bare `||=`
190
+ # would still be nil mid-evaluation, recursing forever.
191
+ @grammar_registry = registry
192
+ # The language now judges itself; every bluebook from here on is
193
+ # judged by that assembled graph, not the raw bootstrap one. Must
194
+ # run after load_grammar_into's ensure clears @bootstrapping.
195
+ while_forcing_fixpoint do
196
+ LANGUAGE_CHAPTERS.each { |name| registry.add_bluebook(call(registry.bluebook(name))) }
197
+ load_attached_grammar_into(registry)
196
198
  end
199
+ # Keyed on this registry's object identity, not a bare boolean, so
200
+ # a manual reset can never leave a stale "ready" from the previous
201
+ # cycle.
202
+ @grammar_ready_for = registry.object_id
203
+ registry
197
204
  end
198
205
 
199
206
  # Whether `grammar_registry`'s memoized singleton has finished its
@@ -35,6 +35,7 @@ require_relative "bluebook/assembly/contract"
35
35
  require_relative "bluebook/assembly/contracts"
36
36
  require_relative "bluebook/assembly/specializer"
37
37
  require_relative "bluebook/assembly/build"
38
+ require_relative "bluebook/assembly/scalars"
38
39
  require_relative "bluebook/assembly/marks"
39
40
  require_relative "bluebook/assembly/aggregate_assembly"
40
41
  require_relative "bluebook/assembly"
@@ -212,6 +212,7 @@ Hecks.bluebook "Deploy" do
212
212
  attribute :volume_gb, VolumeGb
213
213
  attribute :database_class, DatabaseClass
214
214
  attribute :storage_gb, StorageGb
215
+ attribute :cdn_aliases, list_of(CdnAlias), optional: true
215
216
 
216
217
  value_object "DomainName" do
217
218
  attribute :value, String
@@ -251,8 +252,19 @@ Hecks.bluebook "Deploy" do
251
252
  invariant("storage is at most 65536 GB") { value <= 65536 }
252
253
  end
253
254
 
254
- # All six required for the same reason as `FargateTarget.Declare`; this validates a target
255
- # already resolved by `hecks deploy project`'s defaults.
255
+ # A host name the CloudFront edge answers on, such as app.example.com. Lowercase labels only:
256
+ # CloudFront refuses a name its certificate does not cover at deploy, which this cannot see.
257
+ value_object "CdnAlias" do
258
+ attribute :value, String
259
+ invariant("a CDN alias is a lowercase host name such as app.example.com") do
260
+ value.to_s.match?(/\A([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]\z/)
261
+ end
262
+ invariant("a CDN alias is at most 253 characters") { value.to_s.length <= 253 }
263
+ end
264
+
265
+ # The first six are required for the same reason as `FargateTarget.Declare`; this validates a
266
+ # target already resolved by `hecks deploy project`'s defaults. `cdn_aliases` is the one
267
+ # optional setting: naming at least one asks for the CloudFront edge stack.
256
268
  command "Declare" do
257
269
  goal "Validate one domain's AwsBox deploy target before anything is generated from it"
258
270
 
@@ -262,6 +274,7 @@ Hecks.bluebook "Deploy" do
262
274
  attribute :volume_gb, VolumeGb
263
275
  attribute :database_class, DatabaseClass
264
276
  attribute :storage_gb, StorageGb
277
+ attribute :cdn_aliases, list_of(CdnAlias), optional: true
265
278
 
266
279
  sets :domain
267
280
  sets :region
@@ -269,6 +282,7 @@ Hecks.bluebook "Deploy" do
269
282
  sets :volume_gb
270
283
  sets :database_class
271
284
  sets :storage_gb
285
+ sets :cdn_aliases
272
286
 
273
287
  emits "BoxTargetDeclared"
274
288
  end
@@ -2792,7 +2806,7 @@ Hecks.bluebook "Deploy" do
2792
2806
  end
2793
2807
 
2794
2808
  command "ProjectOidc" do
2795
- goal "Write each domain's OIDC client and scope manifest beside it as oidc.json"
2809
+ goal "Write each domain's OIDC client and scope manifest beside it: oidc.json, or one <chapter>.oidc.json for each chapter of a directory that holds several"
2796
2810
 
2797
2811
  attribute :domains, DomainList, optional: true
2798
2812
  attribute :run, RunKey
@@ -0,0 +1,30 @@
1
+ Hecks.behaviors "Change" do
2
+ vision "A change names its script, faults when that script cannot plan it, and is confirmed only from a plan."
3
+
4
+ loads "change.bluebook", "governance.bluebook", "change.hecksagon", "../../../../spec/fixtures/change/governance.hecksagon"
5
+
6
+ test "Request refuses a change that names no script" do
7
+ tests "Request", on: "Change"
8
+ input run: { value: "c1" }, script: { value: "" }
9
+ expect refused: "must match"
10
+ end
11
+
12
+ test "Request refuses a change that is not named" do
13
+ tests "Request", on: "Change"
14
+ input run: { value: "" }, script: { value: "rename" }
15
+ expect refused: "must match"
16
+ end
17
+
18
+ test "Request faults a change whose script nobody registered" do
19
+ tests "Request", on: "Change"
20
+ input run: { value: "c2" }, script: { value: "no_such_script" }
21
+ expect status: "faulted"
22
+ end
23
+
24
+ test "Confirm refuses a change that was never planned" do
25
+ tests "Confirm", on: "Change"
26
+ setup "Change::Change.Request", run: { value: "c3" }, script: { value: "no_such_script" }
27
+ input run: { value: "c3" }, digest: { value: "ab12" }
28
+ expect refused: "the digest confirmed is the digest planned"
29
+ end
30
+ end