hecks 3.0.5 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +24 -12
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +32 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  12. data/lib/hecks/cli/doc_coverage.rb +1 -1
  13. data/lib/hecks/cli/domain_stub.rb +128 -0
  14. data/lib/hecks/cli/domain_writer.rb +39 -0
  15. data/lib/hecks/cli/interview_agent.rb +59 -0
  16. data/lib/hecks/cli/interview_draft.rb +246 -0
  17. data/lib/hecks/cli/interview_run.rb +87 -0
  18. data/lib/hecks/cli/interview_session.rb +299 -0
  19. data/lib/hecks/cli/model_check.rb +1 -1
  20. data/lib/hecks/cli/project_cli.rb +51 -29
  21. data/lib/hecks/cli/reference.rb +2 -1
  22. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  23. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  24. data/lib/hecks/cli/spec_example.rb +1 -1
  25. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  26. data/lib/hecks/corpus.rb +12 -8
  27. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  28. data/lib/hecks/deploy/oidc.json +5 -0
  29. data/lib/hecks/doc/reference.rb +4 -3
  30. data/lib/hecks/doors/cli_door.rb +13 -13
  31. data/lib/hecks/doors/cli_runner.rb +53 -33
  32. data/lib/hecks/doors/launcher_options.rb +5 -5
  33. data/lib/hecks/doors/mcp_door.rb +40 -18
  34. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  35. data/lib/hecks/doors/usage_cache.rb +123 -0
  36. data/lib/hecks/doors.rb +1 -1
  37. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  38. data/lib/hecks/forms/banking_presentation.rb +1 -1
  39. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  40. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  41. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  42. data/lib/hecks/framework.rb +1 -1
  43. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  44. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  45. data/lib/hecks/fuzzing/differential.rb +4 -2
  46. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  47. data/lib/hecks/fuzzing/form_census.rb +2 -2
  48. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  49. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  50. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  51. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  52. data/lib/hecks/gate/stages.yml +5 -5
  53. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  54. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  55. data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
  56. data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
  57. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  58. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  59. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  60. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  61. data/lib/hecks/hecks/adapters/terminal.rb +15 -0
  62. data/lib/hecks/hecks/codebase.bluebook +4 -3
  63. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  64. data/lib/hecks/hecks/custodian.bluebook +81 -3
  65. data/lib/hecks/hecks/hecks.bluebook +18 -0
  66. data/lib/hecks/hecks/hecks.hecksagon +16 -4
  67. data/lib/hecks/hecks/hecks.world +7 -6
  68. data/lib/hecks/hecks/site.behaviors +17 -0
  69. data/lib/hecks/hecks/site.world +10 -0
  70. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  71. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  72. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  73. data/lib/hecks/projections/deploy/box/settings.rb +324 -0
  74. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
  75. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
  76. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  77. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
  78. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
  79. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  80. data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
  81. data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
  82. data/lib/hecks/projections/deploy/box.rb +553 -0
  83. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  84. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  85. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  86. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  87. data/lib/hecks/projections/diagrams.rb +2 -2
  88. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  89. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  90. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  91. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  92. data/lib/hecks/projections/site/edge.rb +136 -0
  93. data/lib/hecks/projections/site/regions.rb +44 -0
  94. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  95. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  96. data/lib/hecks/projections/site/table/checks.rb +124 -0
  97. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  98. data/lib/hecks/projections/site/table.rb +98 -0
  99. data/lib/hecks/projections.rb +2 -0
  100. data/lib/hecks/projector/cli_projector.rb +153 -111
  101. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  102. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  103. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  104. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  105. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  106. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  107. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  108. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  109. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  110. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  111. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  112. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  113. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  114. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  115. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  116. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  117. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  118. data/lib/hecks/runtime/loader.rb +31 -8
  119. data/lib/hecks/runtime/registry/verification.rb +15 -29
  120. data/lib/hecks/runtime/registry.rb +14 -20
  121. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  122. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  123. data/lib/hecks/rust_build/domain_name.rb +26 -0
  124. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  125. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  126. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  127. data/lib/hecks/rust_build/project_rust.rb +57 -24
  128. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  129. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  130. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  131. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  132. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  133. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  134. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  135. data/lib/hecks/storehouse.rb +37 -7
  136. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  137. data/lib/hecks/three_zero/forms.yml +88 -88
  138. data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
  139. data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
  140. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
  141. data/lib/hecks/tools/ci_gates.rb +1 -1
  142. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  143. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  144. data/lib/hecks/tools/sequence_script.rb +2 -2
  145. data/lib/hecks/tools/site_routes.rb +152 -0
  146. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  147. data/lib/hecks/tools.rb +1 -0
  148. data/lib/hecks/version.rb +1 -1
  149. data/lib/hecks.rb +1 -0
  150. data/rust/build/src/cargo_sync.rs +1 -1
  151. data/rust/build/src/pipeline.rs +23 -16
  152. data/rust/build/src/resolve.rs +39 -6
  153. data/rust/codegen/src/json_codec.rs +3 -0
  154. data/rust/codegen/src/naming.rs +4 -0
  155. data/rust/codegen/src/types.rs +45 -0
  156. data/rust/host/HECKS_RELEASE +1 -1
  157. data/rust/host/src/api.rs +1 -1
  158. data/rust/host/src/dispatch.rs +87 -7
  159. data/rust/host/src/needs.rs +111 -0
  160. data/rust/parser/src/keywords.rs +7 -6
  161. data/rust/parser/src/main.rs +4 -3
  162. data/rust/parser/src/parse/chapter.rs +1 -1
  163. data/rust/parser/src/parse/hecksagon.rs +25 -10
  164. data/rust/project/mutations.rb +4 -30
  165. data/rust/project/naming.rb +8 -28
  166. data/rust/project/write_if_changed.rb +4 -80
  167. data/rust/project_rust_pipeline.rb +16 -12
  168. data/rust/src/kernel/cli.rs +2 -0
  169. data/rust/src/kernel/needs.rs +74 -1
  170. metadata +51 -2
@@ -9,7 +9,8 @@ module Hecks
9
9
  # `oidc.json` beside the domain.
10
10
  #
11
11
  # A domain that has no bluebook is skipped and one that cannot be projected is reported, so a
12
- # sweep over the whole checkout carries on past either. `hecks deploy project_oidc` loads
12
+ # sweep over the whole checkout carries on past either. `hecks deploy
13
+ # oidc_manifest.project_oidc` loads
13
14
  # `bundler/setup` first, which pins the `json` gem: newer releases pretty-print an empty array
14
15
  # as `[\n\n]`, a diff spec/oidc_manifest_spec.rb would report as drift.
15
16
  module OidcManifests
@@ -10,8 +10,8 @@ module Hecks
10
10
  #
11
11
  # hecks generate_sequence <domain> [--seed N] [--steps N] [--adversarial fraction]
12
12
  #
13
- # `--adversarial` mutates the sequence as `hecks quality_control ask run` does; 0 (off) by
14
- # default.
13
+ # `--adversarial` mutates the sequence as `hecks quality_control query sweep.run` does; 0 (off)
14
+ # by default.
15
15
  module SequenceScript
16
16
  USAGE = "usage: hecks generate_sequence <domain> [--seed N] [--steps N] [--adversarial FRACTION]"
17
17
 
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hecks"
4
+ require "fileutils"
5
+ require "optparse"
6
+ require_relative "../tools"
7
+ require_relative "../projections/site/routes_ts"
8
+ require_relative "../projections/site/site_cdn"
9
+ require_relative "../projections/site/regions"
10
+
11
+ module Hecks
12
+ module Tools
13
+ # Projects a project's route table into `routes.ts` (`Projections::Site::RoutesTs`) and, when
14
+ # it declares an edge, into the behaviours and listener rules of its template
15
+ # (`Projections::Site::SiteCdn`).
16
+ #
17
+ # hecks site site_projection.project_site [<project>] [--out=<dir>] [--check]
18
+ #
19
+ # `<project>` is the directory whose `bluebook/` holds the chapter that declares the rows and a
20
+ # hecksagon that attaches Site; it defaults to the checkout. `routes.ts` is written to
21
+ # `<project>/generated` unless `--out` names another directory. The template is rewritten in
22
+ # place between its `BEGIN`/`END GENERATED site_cdn` markers, or under `--out` a copy of it is
23
+ # written at the same relative path. With `--check` nothing is written: the tool answers 1 and
24
+ # names each file that differs from the table.
25
+ module SiteRoutes
26
+ # Where the files go, relative to the project, when `--out` names no directory.
27
+ DEFAULT_OUT = "generated"
28
+
29
+ USAGE = "usage: hecks site site_projection.project_site [<project>] [--out=<dir>] [--check]"
30
+
31
+ module_function
32
+
33
+ # Projects the route table, writes what differs and prints a line for each file written.
34
+ #
35
+ # @param argv [Array<String>] the project directory, then the flags
36
+ # @param root [String] the checkout, used as the project when `argv` names none
37
+ # @return [Integer] 0, or 1 when `--check` finds a file out of date
38
+ # @raise [SystemExit] with the reason on stderr when the project declares no route table or
39
+ # the table is refused
40
+ def main(argv, root: Tools::ROOT)
41
+ argv = argv.dup
42
+ out = nil
43
+ check = false
44
+ OptionParser.new do |parser|
45
+ parser.on("--out=DIR") { |value| out = value }
46
+ parser.on("--check") { check = true }
47
+ end.parse!(argv)
48
+ project = argv.empty? ? root : File.expand_path(argv.shift)
49
+ abort USAGE unless argv.empty?
50
+
51
+ files = projection(project, out: out)
52
+ stale = files.reject { |path, text| File.file?(path) && File.read(path) == text }.keys
53
+ return report(stale, project, check) if check
54
+
55
+ stale.each do |path|
56
+ FileUtils.mkdir_p(File.dirname(path))
57
+ File.write(path, files.fetch(path))
58
+ puts "wrote #{display(path, project)}"
59
+ end
60
+ puts "project_site: #{files.size} #{files.size == 1 ? 'file' : 'files'}, current" if stale.empty?
61
+ 0
62
+ end
63
+
64
+ # @param root [String] the project directory
65
+ # @param out [String, nil] the directory to write to; `generated/` of the project when nil
66
+ # @return [Hash{String => String}] each file's absolute path to the text it should hold
67
+ # @raise [SystemExit] when the project declares no route table or the table is refused
68
+ def projection(root, out: nil)
69
+ registry = registry_for(root)
70
+ chapter = Projections::Site::Table.chapter(registry)
71
+ table = Projections::Site::Table.read(chapter, registry: registry)
72
+ files = Projector.call(:site_routes_ts, bluebook: chapter, options: { table: table })
73
+ dir = out ? File.expand_path(out) : File.join(root, DEFAULT_OUT)
74
+ written = files.to_h { |name, text| [File.join(dir, name), text] }
75
+ written.merge(template_files(root, out, chapter, table, registry))
76
+ rescue Projections::Site::Table::Invalid => e
77
+ abort "project_site: #{e.message}"
78
+ end
79
+
80
+ # The template with the edge's regions rewritten, when the project declares an edge.
81
+ #
82
+ # @param root [String] the project directory
83
+ # @param out [String, nil] the directory to write to, or nil to rewrite the template in place
84
+ # @param chapter [Bluebook::Chapter] the chapter that declares the route table
85
+ # @param table [Projections::Site::Table] the checked table
86
+ # @param registry [Hecks::Runtime::Registry] the registry the project booted into
87
+ # @return [Hash{String => String}] the template's path to its text; empty with no edge
88
+ # @raise [SystemExit] when the template is missing or lacks a region
89
+ def template_files(root, out, chapter, table, registry)
90
+ edge = Projections::Site::Edge.read(chapter, table: table, vocabulary: Projections::Site::Table.vocabulary(registry))
91
+ return {} unless edge
92
+
93
+ regions = Projector.call(:site_cdn, bluebook: chapter, options: { table: table, edge: edge })
94
+ relative = edge.setting.template
95
+ source = File.join(root, relative)
96
+ abort "project_site: the template #{relative} does not exist in #{root}" unless File.file?(source)
97
+
98
+ text = regions.reduce(File.read(source)) do |current, (name, block)|
99
+ unless Projections::Site::Regions.region?(current, name)
100
+ abort "project_site: #{relative} has no BEGIN/END GENERATED site_cdn #{name} region"
101
+ end
102
+
103
+ Projections::Site::Regions.replace(current, name, block)
104
+ end
105
+ { (out ? File.join(File.expand_path(out), relative) : source) => text }
106
+ end
107
+
108
+ # Loads the project's chapters the way a deploy does: the bluebooks, then the hecksagons that
109
+ # attach the chapters they need.
110
+ #
111
+ # @param root [String] the project directory
112
+ # @return [Hecks::Runtime::Registry] the registry
113
+ # @raise [SystemExit] when the project has no `bluebook/` directory
114
+ def registry_for(root)
115
+ dir = File.join(root, "bluebook")
116
+ unless Dir.exist?(dir)
117
+ abort "project_site: #{dir} does not exist; a project declares its route table in a bluebook/ directory"
118
+ end
119
+
120
+ registry = Hecks::Runtime::Registry.new(root: root)
121
+ lib_hecks = File.expand_path("..", __dir__)
122
+ Hecks.with_registry(registry) do
123
+ %w[ports/persistence.port ports/extraction.port adapters/driven/memory.adapter
124
+ adapters/driven/prism.adapter].each { |file| Kernel.load(File.join(lib_hecks, file)) }
125
+ Dir.glob(File.join(dir, "*.bluebook")).each { |file| Kernel.load(file) }
126
+ Dir.glob(File.join(dir, "*.hecksagon")).each { |file| Kernel.load(file) }
127
+ end
128
+ registry
129
+ end
130
+
131
+ # @param stale [Array<String>] absolute paths whose text differs from the table
132
+ # @param project [String] the project directory
133
+ # @param check [Boolean] whether this is a `--check` run
134
+ # @return [Integer] 0 when current, else 1 with the stale files on stderr
135
+ def report(stale, project, _check)
136
+ if stale.empty?
137
+ puts "project_site: every file current"
138
+ return 0
139
+ end
140
+
141
+ warn "project_site: out of date: #{stale.map { |path| display(path, project) }.join(', ')} " \
142
+ "(run hecks site site_projection.project_site)"
143
+ 1
144
+ end
145
+
146
+ # @param path [String] an absolute path
147
+ # @param project [String] the project directory
148
+ # @return [String] the path relative to the project when it lies beneath it
149
+ def display(path, project) = path.delete_prefix("#{project}/")
150
+ end
151
+ end
152
+ end
@@ -16,7 +16,7 @@ module Hecks
16
16
  # merge) the base `persisted_by` settings, so `--database` is required. A policy's refusal is
17
17
  # swallowed by `PolicyInterpreter#deliver`, so success is confirmed by query.
18
18
  #
19
- # hecks deploy provision <domain-directory> <slug> --domain=name --realm=name \
19
+ # hecks deploy tenant.provision <domain-directory> <slug> --domain=name --realm=name \
20
20
  # --schema=name --database=name [--adapter=PostgresEra]
21
21
  module TenantProvisioning
22
22
  USAGE = "usage: hecks deploy provision <domain-directory> <slug> --domain=NAME --realm=NAME " \
data/lib/hecks/tools.rb CHANGED
@@ -24,6 +24,7 @@ module Hecks
24
24
  "project_ci_gates" => ["tools/ci_gates", "CiGates"],
25
25
  "decide_ci_gate" => ["tools/ci_gate_decision", "CiGateDecision"],
26
26
  "project_deploy" => ["tools/deploy_recipe", "DeployRecipe"],
27
+ "project_site" => ["tools/site_routes", "SiteRoutes"],
27
28
  "lint_deploy_recipes" => ["tools/deploy_recipe_lint", "DeployRecipeLint"],
28
29
  "project_oidc" => ["tools/oidc_manifests", "OidcManifests"],
29
30
  "project_tenant" => ["tools/tenant_provisioning", "TenantProvisioning"],
data/lib/hecks/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Hecks
2
2
  # Read directly by the gemspec, so this file must never require the
3
3
  # rest of the gem (its dependencies aren't resolved yet at that point).
4
- VERSION = "3.0.5".freeze
4
+ VERSION = "3.1.1".freeze
5
5
  end
data/lib/hecks.rb CHANGED
@@ -29,6 +29,7 @@ require_relative "hecks/projector"
29
29
  require_relative "hecks/projections"
30
30
  # After `Projector`, which dispatches against the `:cli` projection.
31
31
  require_relative "hecks/doors/cli_door"
32
+ require_relative "hecks/doors/usage_cache"
32
33
  require_relative "hecks/doors/cli_runner"
33
34
  require_relative "hecks/storehouse"
34
35
  require_relative "hecks/mcp_stdio_guard"
@@ -50,7 +50,7 @@ fn write_root_mod(out_root: &Path, all_dirs: &[String], domains: &[String], targ
50
50
  out.push_str("// chapter (governance/identity — no merged.rs of its own, so absent\n");
51
51
  out.push_str("// from `domains`) stays unconditional: it has no single feature of its\n");
52
52
  out.push_str("// own to gate behind, and is compiled in by whichever domain(s) attach\n");
53
- out.push_str("// it via `uses_framework`.\n");
53
+ out.push_str("// it via `attaches`.\n");
54
54
  for name in all_dirs {
55
55
  if domains.iter().any(|d| d == name) {
56
56
  out.push_str(&format!("#[cfg(feature = {name:?})]\n"));
@@ -66,10 +66,10 @@ pub fn run(root: &Path, domain: &str, opts: &Options) -> Result<(), String> {
66
66
  }
67
67
  }
68
68
 
69
- let (uses_framework_names, uses_embryonaut_bluebook_names) = match &hecksagon_path {
69
+ let (gem_chapter_names, vendored_package_names) = match &hecksagon_path {
70
70
  Some(p) => (
71
- resolve::resolve_uses_framework(&parser_bin, &target_chapter_name, p)?,
72
- resolve::resolve_uses_embryonaut_bluebook(&parser_bin, &target_chapter_name, p)?,
71
+ resolve::resolve_gem_chapters(&parser_bin, &target_chapter_name, p)?,
72
+ resolve::resolve_vendored_packages(&parser_bin, &target_chapter_name, p)?,
73
73
  ),
74
74
  None => (Vec::new(), Vec::new()),
75
75
  };
@@ -85,45 +85,52 @@ pub fn run(root: &Path, domain: &str, opts: &Options) -> Result<(), String> {
85
85
  lineage_pass::run(&mut target_ir, hecksagon_path.as_deref(), world_path.as_deref(), root)?;
86
86
  let target_ir_text = crate::json::write(&target_ir);
87
87
 
88
- // Every other chapter's `uses_framework` names, resolved through the
88
+ // Every other chapter the gem supplies by `attaches "Name"`, resolved through the
89
89
  // same directory listing (`resolve::framework_members`) the Ruby
90
- // pipeline itself calls.
90
+ // pipeline itself calls. A gem chapter that is not a framework member (the language,
91
+ // Tenancy, Deploy) is loaded by the Ruby runtime alone, so it is checked and skipped.
91
92
  let framework_members = resolve::framework_members(root)?;
93
+ let other_gem_chapters = resolve::other_gem_chapter_names(root)?;
92
94
  let mut chapters: Vec<Chapter> = Vec::new();
93
- for fw_name in &uses_framework_names {
95
+ for fw_name in &gem_chapter_names {
96
+ if other_gem_chapters.contains(fw_name) && !framework_members.iter().any(|(name, _)| name == fw_name) {
97
+ continue;
98
+ }
94
99
  let fw_path = framework_members
95
100
  .iter()
96
101
  .find(|(name, _)| name == fw_name)
97
102
  .map(|(_, path)| path.clone())
98
103
  .ok_or_else(|| {
99
- let known: Vec<&str> = framework_members.iter().map(|(n, _)| n.as_str()).collect();
100
- format!("hecks-build: uses_framework {fw_name:?} names no known framework member — known: {}", known.join(", "))
104
+ let mut known: Vec<&str> = framework_members.iter().map(|(n, _)| n.as_str()).collect();
105
+ known.extend(other_gem_chapters.iter().map(String::as_str));
106
+ known.sort();
107
+ format!("hecks-build: attaches {fw_name:?} names no chapter the gem carries — known: {}", known.join(", "))
101
108
  })?;
102
109
  let fw_chapter_name = resolve::header_chapter_name(&fw_path)?;
103
110
  if &fw_chapter_name != fw_name {
104
111
  return Err(format!(
105
- "hecks-build: {} declares chapter {fw_chapter_name:?}, but uses_framework named {fw_name:?}",
112
+ "hecks-build: {} declares chapter {fw_chapter_name:?}, but attaches named {fw_name:?}",
106
113
  fw_path.display()
107
114
  ));
108
115
  }
109
116
  let fw_ir_text = parse_chapter_with_optionals(&parser_bin, &fw_chapter_name, std::slice::from_ref(&fw_path))?;
110
117
  chapters.push(Chapter {
111
118
  mod_name: fw_name.to_lowercase(),
112
- source_label: format!("{domain} (uses_framework {fw_name:?})"),
119
+ source_label: format!("{domain} (attaches {fw_name:?})"),
113
120
  ir_text: fw_ir_text,
114
121
  });
115
122
  }
116
123
 
117
- // Every vendored package's `uses_embryonaut_bluebook` names, resolved the
124
+ // Every vendored package's `attaches ... from: :vendor` names, resolved the
118
125
  // same way `EmbryonautBluebook.load!` (Ruby) resolves them — without this,
119
126
  // a vendored chapter would silently drop from the output.
120
- for pkg_name in &uses_embryonaut_bluebook_names {
127
+ for pkg_name in &vendored_package_names {
121
128
  let pkg_files = resolve::vendored_bluebook_files(&domain_path, pkg_name)?;
122
129
  let pkg_chapter_name = resolve::header_chapter_name(&pkg_files[0])?;
123
130
  let expected_chapter_name = resolve::pascal(pkg_name);
124
131
  if pkg_chapter_name != expected_chapter_name {
125
132
  return Err(format!(
126
- "hecks-build: {} declares chapter {pkg_chapter_name:?}, but uses_embryonaut_bluebook {pkg_name:?} \
133
+ "hecks-build: {} declares chapter {pkg_chapter_name:?}, but attaches {pkg_name:?}, from: :vendor \
127
134
  expects {expected_chapter_name:?}",
128
135
  pkg_files[0].display()
129
136
  ));
@@ -140,13 +147,13 @@ pub fn run(root: &Path, domain: &str, opts: &Options) -> Result<(), String> {
140
147
  // `expected_chapter_name` above), not the raw `pkg_name` — that
141
148
  // would diverge the moment a package name has an underscore.
142
149
  mod_name: expected_chapter_name.to_lowercase(),
143
- source_label: format!("{domain} (uses_embryonaut_bluebook {pkg_name:?})"),
150
+ source_label: format!("{domain} (attaches {pkg_name:?}, from: :vendor)"),
144
151
  ir_text: pkg_ir_text,
145
152
  });
146
153
  }
147
154
 
148
- // Guards against a `uses_framework`- and a `uses_embryonaut_bluebook`-
149
- // derived entry sharing a `mod_name`, which would silently overwrite one
155
+ // Guards against a gem-chapter and a vendored-package
156
+ // entry sharing a `mod_name`, which would silently overwrite one
150
157
  // chapter's sidecars with another's.
151
158
  {
152
159
  let mut seen: Vec<&str> = Vec::new();
@@ -77,12 +77,45 @@ pub fn grammar_files(root: &Path) -> Result<Vec<PathBuf>, String> {
77
77
  bluebook_files(&dir)
78
78
  }
79
79
 
80
- pub fn resolve_uses_framework(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
81
- resolve_json(parser_bin, chapter_name, hecksagon_path, "uses_framework")
80
+ // The names `attaches "Name"` gave, for a chapter the gem carries.
81
+ pub fn resolve_gem_chapters(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
82
+ resolve_json(parser_bin, chapter_name, hecksagon_path, "gem_chapters")
82
83
  }
83
84
 
84
- pub fn resolve_uses_embryonaut_bluebook(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
85
- resolve_json(parser_bin, chapter_name, hecksagon_path, "uses_embryonaut_bluebook")
85
+ // The names `attaches "name", from: :vendor` gave, for a vendored package.
86
+ pub fn resolve_vendored_packages(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
87
+ resolve_json(parser_bin, chapter_name, hecksagon_path, "vendored_packages")
88
+ }
89
+
90
+ // Where the gem keeps the chapters other than framework members, relative to `lib/hecks`; the
91
+ // same places `Hecks::Chapters::GLOBS` lists.
92
+ const OTHER_GEM_CHAPTER_DIRS: &[&str] = &["language", "grammar", "tenancy/bluebook", "deploy/bluebook", "quality_control"];
93
+
94
+ // The names of the chapters the gem carries beside the framework members, found by reading each
95
+ // file's header. Only the Ruby runtime loads them, so the build uses this list to tell a real
96
+ // gem chapter from a misspelt name.
97
+ pub fn other_gem_chapter_names(root: &Path) -> Result<Vec<String>, String> {
98
+ fn walk(directory: &Path, names: &mut Vec<String>) -> Result<(), String> {
99
+ let Ok(entries) = std::fs::read_dir(directory) else { return Ok(()) };
100
+ for entry in entries {
101
+ let path = entry.map_err(|e| format!("reading {}: {e}", directory.display()))?.path();
102
+ if path.is_dir() {
103
+ walk(&path, names)?;
104
+ } else if path.extension().and_then(|e| e.to_str()) == Some("bluebook") {
105
+ if let Ok(name) = header_chapter_name(&path) {
106
+ names.push(name);
107
+ }
108
+ }
109
+ }
110
+ Ok(())
111
+ }
112
+ let mut names = Vec::new();
113
+ for directory in OTHER_GEM_CHAPTER_DIRS {
114
+ walk(&root.join("lib/hecks").join(directory), &mut names)?;
115
+ }
116
+ names.sort();
117
+ names.dedup();
118
+ Ok(names)
86
119
  }
87
120
 
88
121
  fn resolve_json(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path, key: &str) -> Result<Vec<String>, String> {
@@ -105,13 +138,13 @@ pub fn valid_package_name(name: &str) -> bool {
105
138
  pub fn vendored_bluebook_files(domain: &Path, pkg_name: &str) -> Result<Vec<PathBuf>, String> {
106
139
  if !valid_package_name(pkg_name) {
107
140
  return Err(format!(
108
- "uses_embryonaut_bluebook {pkg_name:?} is not a package name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
141
+ "attaches {pkg_name:?}, from: :vendor is not a package name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
109
142
  ));
110
143
  }
111
144
  let dir = domain.join("vendor").join("embryonaut_bluebooks").join(pkg_name).join("bluebook");
112
145
  if !dir.is_dir() {
113
146
  return Err(format!(
114
- "uses_embryonaut_bluebook {pkg_name:?} names no vendored bluebook at {} — run hecks vendor {pkg_name}",
147
+ "attaches {pkg_name:?}, from: :vendor names no vendored bluebook at {} — run hecks vendor {pkg_name}",
115
148
  dir.display()
116
149
  ));
117
150
  }
@@ -44,6 +44,7 @@ pub fn scalar_json_accessor(scalar_type: &str) -> &'static str {
44
44
  "String" => "as_str",
45
45
  "Integer" => "as_i64",
46
46
  "Float" => "as_f64",
47
+ "TrueClass" | "FalseClass" => "as_bool",
47
48
  other => panic!("no JSON accessor for scalar type {other:?}"),
48
49
  }
49
50
  }
@@ -77,6 +78,7 @@ pub fn scalar_to_json_expr(scalar_type: &str, rust_expr: &str) -> String {
77
78
  "String" => format!("crate::kernel::Json::Str({rust_expr}.clone())"),
78
79
  "Integer" => format!("crate::kernel::Json::int({rust_expr})"),
79
80
  "Float" => format!("crate::kernel::Json::Float({rust_expr})"),
81
+ "TrueClass" | "FalseClass" => format!("crate::kernel::Json::Bool({rust_expr})"),
80
82
  other => panic!("no to_json expr for scalar type {other:?}"),
81
83
  }
82
84
  }
@@ -565,6 +567,7 @@ pub fn emit_closed_set_table_codec(exemplar: &Exemplar, vo: &Json) -> String {
565
567
  Some("String") => format!("crate::kernel::Json::Str(self.{ident}.to_string())"),
566
568
  Some("Integer") => format!("crate::kernel::Json::int(self.{ident})"),
567
569
  Some("Float") => format!("crate::kernel::Json::Float(self.{ident})"),
570
+ Some("TrueClass" | "FalseClass") => format!("crate::kernel::Json::Bool(self.{ident})"),
568
571
  _ => String::new(),
569
572
  };
570
573
  exemplar.render("to_json_field", &[("\"tmpl_field_name\"", naming::ruby_inspect_string(&key)), ("tmpl_json_value_placeholder()", value_expr)])
@@ -7,6 +7,7 @@ pub fn scalar_rust_type(type_name: &str) -> Option<&'static str> {
7
7
  "String" => Some("String"),
8
8
  "Integer" => Some("i64"),
9
9
  "Float" => Some("f64"),
10
+ "TrueClass" | "FalseClass" => Some("bool"),
10
11
  _ => None,
11
12
  }
12
13
  }
@@ -29,6 +30,8 @@ pub fn effective_scalar_type(type_name: &str) -> Option<&'static str> {
29
30
  "String" => Some("String"),
30
31
  "Integer" => Some("Integer"),
31
32
  "Float" => Some("Float"),
33
+ "TrueClass" => Some("TrueClass"),
34
+ "FalseClass" => Some("FalseClass"),
32
35
  _ => None,
33
36
  }
34
37
  }
@@ -250,6 +253,7 @@ pub fn scalar_to_value(type_name: &str, rust_expr: &str) -> Option<String> {
250
253
  "String" => Some(format!("Value::Str({rust_expr}.clone())")),
251
254
  "Integer" => Some(format!("Value::Int({rust_expr})")),
252
255
  "Float" => Some(format!("Value::Float({rust_expr})")),
256
+ "TrueClass" | "FalseClass" => Some(format!("Value::Bool({rust_expr})")),
253
257
  _ => None,
254
258
  }
255
259
  }
@@ -360,3 +360,48 @@ pub fn emit_projected_field_table(aggregate: &Json) -> String {
360
360
  .collect();
361
361
  format!("pub static {name}_PROJECTED_FIELDS: &[crate::kernel::ProjectedFieldSpec] = &[\n{}\n];\n", rows.join("\n"))
362
362
  }
363
+
364
+ #[cfg(test)]
365
+ mod tests {
366
+ use super::*;
367
+
368
+ /// A value object with one attribute per Ruby spelling of a boolean.
369
+ fn flag_value_object() -> Json {
370
+ Json::parse(
371
+ r#"{
372
+ "name":"Entry",
373
+ "attributes":[
374
+ {"name":"on","type":"TrueClass","list":false,"optional":false,"default":false},
375
+ {"name":"off","type":"FalseClass","list":false,"optional":false}
376
+ ]
377
+ }"#,
378
+ )
379
+ .unwrap()
380
+ }
381
+
382
+ #[test]
383
+ fn boolean_attributes_are_rust_bool_not_a_type_named_for_the_ruby_class() {
384
+ let vo = flag_value_object();
385
+ let generated = emit_value_object(&Exemplar::load(), &vo, &HashMap::new(), &HashMap::new());
386
+
387
+ assert!(generated.contains("pub on: bool,"));
388
+ assert!(generated.contains("pub off: bool,"));
389
+ assert!(generated.contains("Value::Bool(self.on)"));
390
+ assert!(!generated.contains("TrueClass,"));
391
+ assert!(!generated.contains("FalseClass,"));
392
+ }
393
+
394
+ #[test]
395
+ fn boolean_attributes_round_trip_through_json_as_bool() {
396
+ let vo = flag_value_object();
397
+ let attributes = vo.get("attributes").map(Json::each).unwrap_or(&[]);
398
+ let by_name = HashMap::new();
399
+ let exemplar = Exemplar::load();
400
+ let from_json = crate::json_codec::emit_from_json_flat(&exemplar, "Entry", attributes, &by_name, None, None, false, false, None);
401
+ let to_json = crate::json_codec::emit_to_json_flat(&exemplar, "Entry", attributes, &by_name, false, &[], None);
402
+
403
+ assert!(from_json.contains("x.as_bool()"));
404
+ assert!(!from_json.contains("TrueClass::from_json"));
405
+ assert!(to_json.contains("crate::kernel::Json::Bool(self.on)"));
406
+ }
407
+ }
@@ -1 +1 @@
1
- 3.0.5
1
+ 3.1.1
data/rust/host/src/api.rs CHANGED
@@ -780,7 +780,7 @@ async fn presentation_save(
780
780
  const PRESENTATION_WRITE_REFUSAL: &str =
781
781
  "this host's kernel carries no ConsoleSettings chapter, so it has no StateStyle/Collection/Overview \
782
782
  commands to dispatch, and writing the rows behind its back would skip the invariants those commands \
783
- enforce. Attach it with `uses_framework \"ConsoleSettings\"` in this domain's own .hecksagon and \
783
+ enforce. Attach it with `attaches \"ConsoleSettings\"` in this domain's own .hecksagon and \
784
784
  rebuild the kernel, or save from the Ruby console engine instead.";
785
785
 
786
786
  // ---- response envelopes ---------------------------------------------