hecks 3.1.3 → 3.2.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
  3. data/lib/hecks/cli/check_era.rb +3 -39
  4. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  5. data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
  6. data/lib/hecks/deploy/oidc.json +15 -0
  7. data/lib/hecks/doors/mcp_door.rb +16 -2
  8. data/lib/hecks/doors/mcp_door_scope.rb +43 -4
  9. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  10. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  11. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  12. data/lib/hecks/hecks/adapters/cost_explorer.adapter +7 -0
  13. data/lib/hecks/hecks/adapters/cost_explorer.rb +102 -0
  14. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  15. data/lib/hecks/hecks/adapters/site_toolchain.rb +21 -11
  16. data/lib/hecks/hecks/hecks.hecksagon +8 -0
  17. data/lib/hecks/language/bluebook/vocabulary.bluebook +7 -7
  18. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  19. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  20. data/lib/hecks/projections/deploy/box/settings.rb +23 -2
  21. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -1
  22. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  23. data/lib/hecks/projections/deploy/box.rb +13 -2
  24. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  25. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  26. data/lib/hecks/projections/site/edge.rb +25 -11
  27. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  28. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  29. data/lib/hecks/projections/site/table/checks.rb +53 -7
  30. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  31. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  32. data/lib/hecks/projections/site/table.rb +16 -5
  33. data/lib/hecks/rust_build/kernel_input.rb +18 -6
  34. data/lib/hecks/rust_build/project_rust.rb +6 -132
  35. data/lib/hecks/site/bluebook/site.bluebook +27 -13
  36. data/lib/hecks/storehouse.rb +35 -1
  37. data/lib/hecks/tools/comment_style.rb +4 -2
  38. data/lib/hecks/tools/deploy_recipe.rb +1 -0
  39. data/lib/hecks/tools/rust_comment_style.rb +1 -1
  40. data/lib/hecks/tools/site_routes.rb +69 -28
  41. data/lib/hecks/version.rb +1 -1
  42. data/lib/hecks/vocabulary.rb +1 -1
  43. data/lib/hecks.rb +1 -0
  44. data/rust/build/src/lineage_pass.rs +1 -1
  45. data/rust/build/src/pipeline.rs +1 -1
  46. data/rust/codegen/src/attr.rs +1 -1
  47. data/rust/codegen/src/bridging.rs +1 -1
  48. data/rust/codegen/src/commands.rs +1 -1
  49. data/rust/codegen/src/constraints.rs +1 -1
  50. data/rust/codegen/src/dependency_planning.rs +1 -1
  51. data/rust/codegen/src/domain_generator.rs +2 -2
  52. data/rust/codegen/src/exemplar.rs +1 -1
  53. data/rust/codegen/src/expr_emitter.rs +1 -1
  54. data/rust/codegen/src/fielded.rs +1 -1
  55. data/rust/codegen/src/json_codec.rs +1 -1
  56. data/rust/codegen/src/main.rs +3 -4
  57. data/rust/codegen/src/mutations.rs +1 -1
  58. data/rust/codegen/src/naming.rs +1 -1
  59. data/rust/codegen/src/ports.rs +1 -1
  60. data/rust/codegen/src/queries.rs +3 -3
  61. data/rust/codegen/src/reactions.rs +1 -1
  62. data/rust/codegen/src/read_models.rs +2 -2
  63. data/rust/codegen/src/reference_specs.rs +1 -1
  64. data/rust/codegen/src/registry.rs +4 -4
  65. data/rust/codegen/src/shared.rs +1 -1
  66. data/rust/codegen/src/skip_reason.rs +1 -1
  67. data/rust/codegen/src/types.rs +4 -4
  68. data/rust/host/HECKS_RELEASE +1 -1
  69. data/rust/host/src/auth.rs +25 -1
  70. data/rust/host/src/needs.rs +94 -27
  71. data/rust/host/src/web.rs +9 -5
  72. data/rust/src/exemplar/commands.rs +1 -1
  73. data/rust/src/exemplar/constraints.rs +1 -1
  74. data/rust/src/exemplar/fielded.rs +1 -1
  75. data/rust/src/exemplar/json.rs +1 -1
  76. data/rust/src/exemplar/mod.rs +1 -1
  77. data/rust/src/exemplar/mutations.rs +1 -1
  78. data/rust/src/exemplar/queries.rs +1 -1
  79. data/rust/src/exemplar/reactions.rs +1 -1
  80. data/rust/src/exemplar/read_models.rs +1 -1
  81. data/rust/src/exemplar/registry.rs +1 -1
  82. data/rust/src/exemplar/types.rs +1 -1
  83. data/rust/src/kernel/cli.rs +2 -2
  84. data/rust/src/kernel/read_model.rs +1 -1
  85. data/rust/src/lib.rs +1 -1
  86. metadata +5 -23
  87. data/rust/project/bridging.rb +0 -257
  88. data/rust/project/commands.rb +0 -877
  89. data/rust/project/constraints.rb +0 -86
  90. data/rust/project/dependency_planning.rb +0 -178
  91. data/rust/project/domain_generator.rb +0 -983
  92. data/rust/project/exemplar.rb +0 -148
  93. data/rust/project/expr_emitter.rb +0 -72
  94. data/rust/project/fielded.rb +0 -141
  95. data/rust/project/json_codec.rb +0 -641
  96. data/rust/project/mutations.rb +0 -695
  97. data/rust/project/naming.rb +0 -223
  98. data/rust/project/ports.rb +0 -65
  99. data/rust/project/queries.rb +0 -493
  100. data/rust/project/reactions.rb +0 -317
  101. data/rust/project/read_models.rb +0 -690
  102. data/rust/project/reference_specs.rb +0 -27
  103. data/rust/project/registry.rb +0 -396
  104. data/rust/project/skip_reason.rb +0 -29
  105. data/rust/project/types.rb +0 -225
  106. data/rust/project/write_if_changed.rb +0 -7
  107. data/rust/project.rb +0 -30
  108. data/rust/project_rust_pipeline.rb +0 -391
@@ -252,6 +252,7 @@ module Hecks
252
252
  # @return [void]
253
253
  def write(artifact, out_dir)
254
254
  written = Hecks::Projector.write(artifact, out_dir, as: :files)
255
+ written.each { |path| File.chmod(0o755, path) if path.end_with?(".sh") }
255
256
 
256
257
  written.each { |path| puts "wrote #{path}" }
257
258
  puts
@@ -30,7 +30,7 @@ module Hecks
30
30
  DOC_LINE = %r{\A\s*(?:///(?!/)|//!)}
31
31
 
32
32
  # `// TMPL:name BEGIN`/`END` sentinels are parsed verbatim by
33
- # `rust/project/exemplar.rb`; every prose rule skips them outright.
33
+ # `rust/codegen/src/exemplar.rs`; every prose rule skips them outright.
34
34
  TMPL_MARKER = /\bTMPL:\S+\s+(?:BEGIN|END)\b/i
35
35
  MOD_DOC_LINE = %r{\A\s*//!}
36
36
  ATTR_LINE = /\A\s*#\[/
@@ -14,41 +14,47 @@ module Hecks
14
14
  # it declares an edge, into the behaviours and listener rules of its template
15
15
  # (`Projections::Site::SiteCdn`).
16
16
  #
17
- # hecks site site_projection.project_site [<project>] [--out=<dir>] [--check]
17
+ # hecks site site_projection.project_site [<project>] [--out=<dir>] [--template=<file>]
18
+ # [--extension=ts|mts] [--check]
18
19
  #
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.
20
+ # `<project>` (default: the working directory) holds `bluebook/` and the `vendor/` it attaches
21
+ # from. `routes.ts` goes to `<project>/generated` or to `--out`, anywhere. The template is the
22
+ # `Edge` row's, relative to the project, or `--template`, anywhere; it is rewritten in place
23
+ # between its `BEGIN`/`END GENERATED site_cdn` markers. With `--out` alone, `--out` receives a
24
+ # copy of it instead. `--extension=mts` names the module `routes.mts`; `--check` writes nothing.
25
25
  module SiteRoutes
26
26
  # Where the files go, relative to the project, when `--out` names no directory.
27
27
  DEFAULT_OUT = "generated"
28
28
 
29
- USAGE = "usage: hecks site site_projection.project_site [<project>] [--out=<dir>] [--check]"
29
+ # The extensions the module may take; the first is the default.
30
+ EXTENSIONS = %w[ts mts].freeze
31
+
32
+ USAGE = "usage: hecks site site_projection.project_site [<project>] [--out=<dir>] " \
33
+ "[--template=<file>] [--extension=#{EXTENSIONS.join('|')}] [--check]".freeze
30
34
 
31
35
  module_function
32
36
 
33
37
  # Projects the route table, writes what differs and prints a line for each file written.
34
38
  #
35
39
  # @param argv [Array<String>] the project directory, then the flags
36
- # @param root [String] the checkout, used as the project when `argv` names none
40
+ # @param root [String] the project used when `argv` names none; the working directory
37
41
  # @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)
42
+ # @raise [SystemExit] with the reason on stderr when the project declares no route table, the
43
+ # table is refused, or a flag is refused
44
+ def main(argv, root: Dir.pwd)
41
45
  argv = argv.dup
42
- out = nil
46
+ out = template = extension = nil
43
47
  check = false
44
48
  OptionParser.new do |parser|
45
49
  parser.on("--out=DIR") { |value| out = value }
50
+ parser.on("--template=FILE") { |value| template = value }
51
+ parser.on("--extension=EXT") { |value| extension = value }
46
52
  parser.on("--check") { check = true }
47
53
  end.parse!(argv)
48
54
  project = argv.empty? ? root : File.expand_path(argv.shift)
49
55
  abort USAGE unless argv.empty?
50
56
 
51
- files = projection(project, out: out)
57
+ files = projection(project, out: out, template: template, extension: extension)
52
58
  stale = files.reject { |path, text| File.file?(path) && File.read(path) == text }.keys
53
59
  return report(stale, project, check) if check
54
60
 
@@ -63,16 +69,26 @@ module Hecks
63
69
 
64
70
  # @param root [String] the project directory
65
71
  # @param out [String, nil] the directory to write to; `generated/` of the project when nil
72
+ # @param template [String, nil] the template to rewrite in place, wherever it lies; the
73
+ # `Edge` row's template, relative to the project, when nil
74
+ # @param extension [String, nil] `ts` (the default) or `mts`, the module's file extension
66
75
  # @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)
76
+ # @raise [SystemExit] when the project declares no route table, the table is refused, or a
77
+ # path or the extension is refused
78
+ def projection(root, out: nil, template: nil, extension: nil)
79
+ extension ||= EXTENSIONS.first
80
+ unless EXTENSIONS.include?(extension)
81
+ abort "project_site: --extension is one of #{EXTENSIONS.join(', ')}, not #{extension.inspect}"
82
+ end
83
+ dir = out ? File.expand_path(out) : File.join(root, DEFAULT_OUT)
84
+ abort "project_site: --out #{dir} is a file; it names the directory routes.#{extension} goes in" if File.file?(dir)
85
+
69
86
  registry = registry_for(root)
70
87
  chapter = Projections::Site::Table.chapter(registry)
71
88
  table = Projections::Site::Table.read(chapter, registry: registry)
72
89
  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))
90
+ written = files.to_h { |name, text| [File.join(dir, name.sub(/\.ts\z/, ".#{extension}")), text] }
91
+ written.merge(template_files(root, out, chapter, table, registry, template: template))
76
92
  rescue Projections::Site::Table::Invalid => e
77
93
  abort "project_site: #{e.message}"
78
94
  end
@@ -84,25 +100,50 @@ module Hecks
84
100
  # @param chapter [Bluebook::Chapter] the chapter that declares the route table
85
101
  # @param table [Projections::Site::Table] the checked table
86
102
  # @param registry [Hecks::Runtime::Registry] the registry the project booted into
103
+ # @param template [String, nil] a template named on the command line, rewritten in place
87
104
  # @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
105
+ # @raise [SystemExit] when a template is named and the project declares no edge, the
106
+ # template is missing or lacks a region it needs, or holds a region an edge without a load
107
+ # balancer does not use
108
+ def template_files(root, out, chapter, table, registry, template: nil)
109
+ edge = Projections::Site::Edge.read(chapter, table: table, template: template,
110
+ vocabulary: Projections::Site::Table.vocabulary(registry))
111
+ unless edge
112
+ abort "project_site: --template names #{template}, but the project declares no Edge rows" if template
113
+ return {}
114
+ end
92
115
 
93
116
  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)
117
+ relative = template || edge.setting.template
118
+ source = template ? File.expand_path(template) : File.join(root, relative)
119
+ unless File.file?(source)
120
+ abort "project_site: the template #{relative} does not exist#{" in #{root}" unless template}"
121
+ end
97
122
 
98
- text = regions.reduce(File.read(source)) do |current, (name, block)|
123
+ text = rewritten(File.read(source), regions, relative, edge)
124
+ in_place = template || out.nil?
125
+ { (in_place ? source : File.join(File.expand_path(out), relative)) => text }
126
+ end
127
+
128
+ # @param text [String] the template
129
+ # @param regions [Hash{String => String}] each region's name to the block that goes in it
130
+ # @param relative [String] the template's name, for a message
131
+ # @param edge [Projections::Site::Edge] the checked edge
132
+ # @return [String] the template with each region rewritten
133
+ # @raise [SystemExit] when a region is missing, or a listener_rules region is left behind
134
+ # by an edge with no load balancer
135
+ def rewritten(text, regions, relative, edge)
136
+ if !edge.alb? && Projections::Site::Regions.region?(text, "listener_rules")
137
+ abort "project_site: #{relative} has a BEGIN/END GENERATED site_cdn listener_rules region, " \
138
+ "and the Edge row says alb: false; remove the region"
139
+ end
140
+ regions.reduce(text) do |current, (name, block)|
99
141
  unless Projections::Site::Regions.region?(current, name)
100
142
  abort "project_site: #{relative} has no BEGIN/END GENERATED site_cdn #{name} region"
101
143
  end
102
144
 
103
145
  Projections::Site::Regions.replace(current, name, block)
104
146
  end
105
- { (out ? File.join(File.expand_path(out), relative) : source) => text }
106
147
  end
107
148
 
108
149
  # Loads the project's chapters the way a deploy does: the bluebooks, then the hecksagons that
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.1.3".freeze
4
+ VERSION = "3.2.1".freeze
5
5
  end
@@ -51,7 +51,7 @@ module Hecks
51
51
  ].freeze,
52
52
  "CiGate" => [
53
53
  {"name"=>"runtime_changed", "workflow"=>"ci.yml", "mode"=>"touches", "pattern"=>"^lib/hecks/runtime/", "push"=>"skip", "label"=>"does this change touch lib/hecks/runtime/**?"}.freeze,
54
- {"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "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$)", "push"=>"before_sha", "label"=>"does this change touch anything rspec_postgres_io_parallel covers?"}.freeze
54
+ {"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "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$)", "push"=>"before_sha", "label"=>"does this change touch anything rspec_postgres_io_parallel covers?"}.freeze
55
55
  ].freeze,
56
56
  "Comparison" => [
57
57
  {"symbol"=>">=", "compares_less_than"=>"true", "compares_equal"=>"false", "negated"=>"true"}.freeze,
data/lib/hecks.rb CHANGED
@@ -34,6 +34,7 @@ require_relative "hecks/doors/cli_runner"
34
34
  require_relative "hecks/storehouse"
35
35
  require_relative "hecks/mcp_stdio_guard"
36
36
  require_relative "hecks/doors/mcp_door_scope"
37
+ require_relative "hecks/doors/mcp_guide_cache"
37
38
  require_relative "hecks/framework"
38
39
  require_relative "hecks/vendoring"
39
40
  require_relative "hecks/embryonaut_bluebook"
@@ -1,5 +1,5 @@
1
1
  //! Rust port of `Exporter.lineage` (`lib/hecks/projector/exporter.rb`), by way
2
- //! of `rust/project_rust_pipeline.rb::derive_lineage`.
2
+ //! of the retired Ruby pipeline's `derive_lineage`.
3
3
 
4
4
  use std::collections::HashMap;
5
5
  use std::path::Path;
@@ -1,4 +1,4 @@
1
- //! Rust port of `RustProjectPipeline.call` (`rust/project_rust_pipeline.rb`) —
1
+ //! Rust port of the retired Ruby pipeline's `RustProjectPipeline.call` —
2
2
  //! this crate's own orchestration core.
3
3
 
4
4
  use std::path::{Path, PathBuf};
@@ -1,5 +1,5 @@
1
1
  //! Free-function accessors over an `IR::Attribute#to_h`-shaped `Json::Object`, mirroring the
2
- //! bare Hash lookups in rust/project/*.rb.
2
+ //! bare Hash lookups the retired Ruby generator used.
3
3
 
4
4
  use crate::json::Json;
5
5
 
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/bridging.rb`: cross-type coercion checks for `:set` and `:append` RHS.
1
+ //! Port of the retired Ruby generator's `bridging.rb`: cross-type coercion checks for `:set` and `:append` RHS.
2
2
 
3
3
  use crate::json::Json;
4
4
  use crate::literal::Literal;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/commands.rb`: emits the per-command dispatch functions.
1
+ //! Port of the retired Ruby generator's `commands.rb`: emits the per-command dispatch functions.
2
2
 
3
3
  use crate::exemplar::Exemplar;
4
4
  use crate::json::Json;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/constraints.rb`: `admits:` and `pattern:` check emission.
1
+ //! Port of the retired Ruby generator's `constraints.rb`: `admits:` and `pattern:` check emission.
2
2
 
3
3
  use crate::exemplar::Exemplar;
4
4
  use crate::json::Json;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/dependency_planning.rb`, an independent re-derivation of the
1
+ //! Port of the retired Ruby generator's `dependency_planning.rb`, an independent re-derivation of the
2
2
  //! Analyzer's `complete_state? && state_independent?` predicate. Mirrors the Ruby file.
3
3
 
4
4
  use crate::json::Json;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/domain_generator.rb`'s `DomainGenerator.call`: the per-aggregate
1
+ //! Port of the retired Ruby generator's `domain_generator.rb`'s `DomainGenerator.call`: the per-aggregate
2
2
  //! `.rs` files plus `registry.rs`. `metadata.rs` and `ir.json` are not emitted here.
3
3
 
4
4
  use crate::exemplar::Exemplar;
@@ -1181,7 +1181,7 @@ pub fn generate(
1181
1181
  None,
1182
1182
  None,
1183
1183
  Some(format!(
1184
- "read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::append_lineage_mutation — both generic over storage_name (\"{}\"), dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own CommandInterpreter routing for a Postgres-bound aggregate (rust/project.rb's own header)",
1184
+ "read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::append_lineage_mutation — both generic over storage_name (\"{}\"), dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own CommandInterpreter routing for a Postgres-bound aggregate (rust/codegen/src/domain_generator.rs)",
1185
1185
  lineage_aggregate.get("storage_name").map(Json::to_s).unwrap_or_default()
1186
1186
  )),
1187
1187
  );
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/exemplar.rb`: loads the template shapes in `rust/src/exemplar/*.rs`.
1
+ //! Port of the retired Ruby generator's `exemplar.rb`: loads the template shapes in `rust/src/exemplar/*.rs`.
2
2
  //! Both codegens read those files at runtime, so the shapes cannot drift.
3
3
  use std::collections::HashMap;
4
4
  use std::path::PathBuf;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/expr_emitter.rb`: turns each IR rule's `ast` tree into `Expr` source.
1
+ //! Port of the retired Ruby generator's `expr_emitter.rb`: turns each IR rule's `ast` tree into `Expr` source.
2
2
  //! A literal-array `include?` never reaches here; `AstJson` rewrites it to an or of equalities.
3
3
 
4
4
  use crate::json::Json;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/fielded.rb`: emits the `Fielded` impls for structs and records.
1
+ //! Port of the retired Ruby generator's `fielded.rb`: emits the `Fielded` impls for structs and records.
2
2
 
3
3
  use crate::exemplar::Exemplar;
4
4
  use crate::json::Json;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/json_codec.rb`, the JSON boundary generator: `to_json`/`from_json`
1
+ //! Port of the retired Ruby generator's `json_codec.rb`, the JSON boundary generator: `to_json`/`from_json`
2
2
  //! codecs, closed-set codecs, and `extract_id`/`extract_wants`/`self_identity`.
3
3
 
4
4
  use crate::exemplar::Exemplar;
@@ -1,4 +1,4 @@
1
- //! `hecks-codegen` — Rust port of the IR-to-Rust-source codegen in `rust/project/*.rb`.
1
+ //! `hecks-codegen` — the IR-to-Rust-source generator, the only one (ADR 0086).
2
2
  //! Usage: `hecks-codegen <prelude|domain|full> ...`, one subcommand per function below.
3
3
  // Some ported `naming.rs` helpers are not called yet.
4
4
  #![allow(dead_code)]
@@ -110,7 +110,7 @@ fn write_domain(
110
110
  mod_name: &str,
111
111
  out_dir: &str,
112
112
  ) -> Result<domain_generator::GeneratedDomain, String> {
113
- // Refuse before writing anything; same message as `RustProjection::DomainGenerator.call`.
113
+ // Refuse before writing anything, so a name collision is reported before any file exists.
114
114
  let aggregate_names: Vec<&str> = ir
115
115
  .get("aggregates")
116
116
  .map(Json::each)
@@ -257,8 +257,7 @@ fn run_full(args: &[String]) -> Result<(), String> {
257
257
  puts_blank(&mut merged_rs);
258
258
  puts_str(&mut merged_rs, &reactions::emit_identity_head_table(&ex, &merged_aggregates));
259
259
  puts_blank(&mut merged_rs);
260
- // Section order must match `rust/project/domain_generator.rb`; the pipeline spec compares
261
- // this file byte for byte.
260
+ // Section order is part of the output: `hecks regenerate_corpus --check` diffs this file byte for byte.
262
261
  puts_str(&mut merged_rs, &reactions::emit_entity_identity_head_table(&ex, &merged_aggregates));
263
262
  puts_blank(&mut merged_rs);
264
263
  puts_str(&mut merged_rs, &reactions::emit_command_attributes_table(&ex, &merged_aggregates));
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/mutations.rb`, function for function.
1
+ //! Port of the retired Ruby generator's `mutations.rb`, function for function.
2
2
  //! `mark_append_optional_fields!` is not ported: `Json` has no mutation API and the corpus
3
3
  //! already declares `optional: true` on every field that pass would touch.
4
4
 
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/naming.rb`, mirrored function for function.
1
+ //! Port of the retired Ruby generator's `naming.rb`, mirrored function for function.
2
2
 
3
3
  use crate::json::Json;
4
4
 
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/ports.rb` — read that file's own header comments
1
+ //! Port of the retired Ruby generator's `ports.rb` — read that file's own header comments
2
2
  //! in full; this mirrors its algorithm directly, function for function.
3
3
 
4
4
  use crate::exemplar::Exemplar;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/queries.rb`, mirrored function for function.
1
+ //! Port of the retired Ruby generator's `queries.rb`, mirrored function for function.
2
2
 
3
3
  use crate::exemplar::Exemplar;
4
4
  use crate::json::Json;
@@ -302,7 +302,7 @@ pub fn query_skip_reason(query: &Json, aggregate: &Json, value_objects_by_name:
302
302
  let extra_keys = ["cursor", "consistency", "freshness", "inspection"];
303
303
  let extras: Vec<&str> = extra_keys.iter().filter(|k| query.get(k).is_some()).copied().collect();
304
304
  if !extras.is_empty() {
305
- return Some(skip(extras[0], format!("declares {} — out of scope for this generator (rust/project/queries.rb's own header has the full argument)", extras.join(", "))));
305
+ return Some(skip(extras[0], format!("declares {} — out of scope for this generator (rust/codegen/src/queries.rs's own header has the full argument)", extras.join(", "))));
306
306
  }
307
307
  if query.get("index_hints").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
308
308
  return Some(skip("index_hints", "declares use_index, out of scope for the same reason the extras above are"));
@@ -780,7 +780,7 @@ pub fn emit_query_arg_check_table(query_defs: &[QueryDef]) -> String {
780
780
  })
781
781
  .collect();
782
782
  format!(
783
- "/// C3.7 for a named query's own arguments — `query_arg_checks`\n/// (rust/project/queries.rb) has the full story.\npub fn check_query_args(verb: &str, args: &crate::kernel::Json) -> Result<(), crate::kernel::Refusal> {{\n match verb {{\n{}\n _ => Ok(()),\n }}\n}}\n",
783
+ "/// C3.7 for a named query's own arguments — `query_arg_checks`\n/// (rust/codegen/src/queries.rs) has the full story.\npub fn check_query_args(verb: &str, args: &crate::kernel::Json) -> Result<(), crate::kernel::Refusal> {{\n match verb {{\n{}\n _ => Ok(()),\n }}\n}}\n",
784
784
  arms.join("\n")
785
785
  )
786
786
  }
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/reactions.rb`, mirrored function for function.
1
+ //! Port of the retired Ruby generator's `reactions.rb`, mirrored function for function.
2
2
 
3
3
  use crate::exemplar::Exemplar;
4
4
  use crate::json::Json;
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/read_models.rb`, mirrored function for function —
1
+ //! Port of the retired Ruby generator's `read_models.rb`, mirrored function for function —
2
2
  //! read that file's own header for the full algorithm.
3
3
 
4
4
  use crate::exemplar::Exemplar;
@@ -267,7 +267,7 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
267
267
  None
268
268
  }
269
269
 
270
- // Mirrors `rust/project/read_models.rb`'s `group_by_skip_reason`: the one
270
+ // Mirrors the retired Ruby generator's `read_models.rb`'s `group_by_skip_reason`: the one
271
271
  // shape the corpus declares — a single rootless head, group_by alone.
272
272
  fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
273
273
  let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
@@ -1,4 +1,4 @@
1
- //! Port of `rust/project/reference_specs.rb` — read that file's own
1
+ //! Port of the retired Ruby generator's `reference_specs.rb` — read that file's own
2
2
  //! header comments in full; this mirrors its algorithm directly,
3
3
  //! function for function.
4
4
 
@@ -1,4 +1,4 @@
1
- //! Command router emitter, ported from `rust/project/registry.rb`.
1
+ //! Command router emitter, ported from the retired Ruby generator's `registry.rb`.
2
2
  //! The entry structs mirror the Hashes `domain_generator.rb` accumulates while walking the IR.
3
3
 
4
4
  use crate::exemplar::Exemplar;
@@ -144,7 +144,7 @@ pub fn emit_role_check(
144
144
  ))
145
145
  }
146
146
 
147
- /// Port of `rust/project/registry.rb#emit_tenant_boundary_check`.
147
+ /// Port of the retired Ruby generator's `registry.rb#emit_tenant_boundary_check`.
148
148
  /// Hand-built: the target accessor differs for a single-attribute value object and a bare scalar.
149
149
  pub fn emit_tenant_boundary_check(check: &TenantBoundaryCheck) -> String {
150
150
  let ref_ident = naming::rust_ident_field(&check.reference_field);
@@ -740,7 +740,7 @@ pub fn emit_registry(exemplar: &Exemplar, aggregates: &[AggregateEntry]) -> Stri
740
740
  format!("{header}{body}")
741
741
  }
742
742
 
743
- /// Port of `rust/project/registry.rb#emit_reference_table`.
743
+ /// Port of the retired Ruby generator's `registry.rb#emit_reference_table`.
744
744
  pub fn emit_reference_table(aggregates: &[AggregateEntry]) -> String {
745
745
  let rows: Vec<String> = aggregates
746
746
  .iter()
@@ -760,7 +760,7 @@ pub fn emit_reference_table(aggregates: &[AggregateEntry]) -> String {
760
760
  )
761
761
  }
762
762
 
763
- /// Port of `rust/project/registry.rb#emit_reference_lookup`.
763
+ /// Port of the retired Ruby generator's `registry.rb#emit_reference_lookup`.
764
764
  pub fn emit_reference_lookup(aggregates: &[AggregateEntry]) -> String {
765
765
  let arms: Vec<String> = aggregates
766
766
  .iter()
@@ -1,4 +1,4 @@
1
- //! Predicates ported from `rust/project/mutations.rb` that `types.rs` and `json_codec.rs` share.
1
+ //! Predicates ported from the retired Ruby generator's `mutations.rb` that `types.rs` and `json_codec.rs` share.
2
2
 
3
3
  use crate::json::Json;
4
4
  use std::collections::HashMap;
@@ -1,4 +1,4 @@
1
- //! A skip decision plus the construct family that forced it (`rust/project/skip_reason.rb`).
1
+ //! A skip decision plus the construct family that forced it (the retired Ruby generator's `skip_reason.rb`).
2
2
  //! `construct` goes to `manifest.json` beside the reason so tools read the family, not the prose.
3
3
 
4
4
  use std::fmt;
@@ -1,4 +1,4 @@
1
- //! Type emitters ported from `rust/project/types.rb`: records, entities, value objects and
1
+ //! Type emitters ported from the retired Ruby generator's `types.rb`: records, entities, value objects and
2
2
  //! closed-set tables.
3
3
 
4
4
  use crate::exemplar::Exemplar;
@@ -286,7 +286,7 @@ pub fn emit_record(exemplar: &Exemplar, aggregate: &Json, value_objects_by_name:
286
286
  format!("{struct_part}\n\n{fielded_part}")
287
287
  }
288
288
 
289
- /// Port of `rust/project/types.rb#projected_field_pseudo_attributes` (ADR 0025).
289
+ /// Port of the retired Ruby generator's `types.rb#projected_field_pseudo_attributes` (ADR 0025).
290
290
  pub fn projected_field_pseudo_attributes(aggregate: &Json) -> Vec<Json> {
291
291
  aggregate
292
292
  .get("projected_fields")
@@ -318,7 +318,7 @@ pub fn with_projected_field_pseudo_attributes(aggregate: &Json) -> Json {
318
318
  Json::Object(pairs)
319
319
  }
320
320
 
321
- /// Port of `rust/project/types.rb#emit_set_projected_field`.
321
+ /// Port of the retired Ruby generator's `types.rb#emit_set_projected_field`.
322
322
  pub fn emit_set_projected_field(aggregate: &Json) -> String {
323
323
  let name = naming::rust_ident(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
324
324
  let arms: Vec<String> = aggregate
@@ -338,7 +338,7 @@ pub fn emit_set_projected_field(aggregate: &Json) -> String {
338
338
  )
339
339
  }
340
340
 
341
- /// Port of `rust/project/types.rb#emit_projected_field_table`.
341
+ /// Port of the retired Ruby generator's `types.rb#emit_projected_field_table`.
342
342
  pub fn emit_projected_field_table(aggregate: &Json) -> String {
343
343
  let name = naming::screaming_snake(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
344
344
  let rows: Vec<String> = aggregate
@@ -1 +1 @@
1
- 3.1.3
1
+ 3.2.1
@@ -592,8 +592,25 @@ pub async fn active_role(client: &Mutex<Client>, domain_ir: &Value, email: &str)
592
592
  .and_then(|(_, state)| role_of(state).map(String::from)))
593
593
  }
594
594
 
595
+ /// The Governance identity id linked to the member with `email`, read from
596
+ /// the live membership head. `None` when the member has not signed in yet.
597
+ pub async fn member_identity_id(client: &Mutex<Client>, domain_ir: &Value, email: &str) -> anyhow::Result<Option<String>> {
598
+ let email = email.to_lowercase();
599
+ Ok(member_rows(client, domain_ir)
600
+ .await?
601
+ .iter()
602
+ .find(|(id, state)| id.to_lowercase() == email && !is_deleted(state))
603
+ .and_then(|(_, state)| linked_identity_id(state)))
604
+ }
605
+
606
+ // The id the sign-in flow wrote onto a Member row, or `None` before then.
607
+ fn linked_identity_id(state: &Value) -> Option<String> {
608
+ state.get("identity_id").and_then(|v| v.get("value")).and_then(|v| v.as_str()).map(str::to_string)
609
+ }
610
+
595
611
  /// Every admitted person as a JSON row: name, email, role, `linked`
596
- /// (signed in), `granted` (has access), and `disabled`.
612
+ /// (signed in), `granted` (has access), `disabled`, and `identity_id`
613
+ /// (the Governance identity id, `null` until the person has signed in).
597
614
  pub async fn all_people(client: &Mutex<Client>, domain_ir: &Value) -> anyhow::Result<Vec<Value>> {
598
615
  Ok(member_rows(client, domain_ir)
599
616
  .await?
@@ -610,6 +627,7 @@ pub async fn all_people(client: &Mutex<Client>, domain_ir: &Value) -> anyhow::Re
610
627
  "linked": linked,
611
628
  "granted": role.is_some() && !disabled,
612
629
  "disabled": disabled,
630
+ "identity_id": linked_identity_id(&state),
613
631
  })
614
632
  })
615
633
  .collect())
@@ -1206,9 +1224,15 @@ mod tests {
1206
1224
  let chris = people.iter().find(|p| p["email"] == "chris@example.com").unwrap();
1207
1225
  assert_eq!(chris["linked"], true);
1208
1226
  assert_eq!(chris["granted"], true);
1227
+ assert_eq!(chris["identity_id"], "id-1");
1209
1228
  let angie = people.iter().find(|p| p["email"] == "angie@example.com").unwrap();
1210
1229
  assert_eq!(angie["linked"], false);
1211
1230
  assert_eq!(angie["granted"], false);
1231
+ assert!(angie["identity_id"].is_null());
1232
+
1233
+ assert_eq!(member_identity_id(&db, &domain_ir, "Chris@Example.com").await.unwrap().as_deref(), Some("id-1"));
1234
+ assert_eq!(member_identity_id(&db, &domain_ir, "angie@example.com").await.unwrap(), None);
1235
+ assert_eq!(member_identity_id(&db, &domain_ir, "nobody@example.com").await.unwrap(), None);
1212
1236
  }
1213
1237
 
1214
1238
  #[tokio::test]