hecks 3.2.0 → 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 (99) 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/doors/mcp_door.rb +16 -2
  5. data/lib/hecks/doors/mcp_door_scope.rb +43 -4
  6. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  7. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  8. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  9. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  10. data/lib/hecks/hecks/adapters/site_toolchain.rb +21 -11
  11. data/lib/hecks/language/bluebook/vocabulary.bluebook +7 -7
  12. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  13. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  14. data/lib/hecks/projections/deploy/box/settings.rb +23 -2
  15. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  16. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  17. data/lib/hecks/projections/deploy/box.rb +13 -2
  18. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  19. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  20. data/lib/hecks/projections/site/edge.rb +25 -11
  21. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  22. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  23. data/lib/hecks/projections/site/table/checks.rb +53 -7
  24. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  25. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  26. data/lib/hecks/projections/site/table.rb +16 -5
  27. data/lib/hecks/rust_build/kernel_input.rb +18 -6
  28. data/lib/hecks/rust_build/project_rust.rb +6 -132
  29. data/lib/hecks/site/bluebook/site.bluebook +27 -13
  30. data/lib/hecks/storehouse.rb +35 -1
  31. data/lib/hecks/tools/comment_style.rb +4 -2
  32. data/lib/hecks/tools/rust_comment_style.rb +1 -1
  33. data/lib/hecks/tools/site_routes.rb +69 -28
  34. data/lib/hecks/version.rb +1 -1
  35. data/lib/hecks/vocabulary.rb +1 -1
  36. data/lib/hecks.rb +1 -0
  37. data/rust/build/src/lineage_pass.rs +1 -1
  38. data/rust/build/src/pipeline.rs +1 -1
  39. data/rust/codegen/src/attr.rs +1 -1
  40. data/rust/codegen/src/bridging.rs +1 -1
  41. data/rust/codegen/src/commands.rs +1 -1
  42. data/rust/codegen/src/constraints.rs +1 -1
  43. data/rust/codegen/src/dependency_planning.rs +1 -1
  44. data/rust/codegen/src/domain_generator.rs +2 -2
  45. data/rust/codegen/src/exemplar.rs +1 -1
  46. data/rust/codegen/src/expr_emitter.rs +1 -1
  47. data/rust/codegen/src/fielded.rs +1 -1
  48. data/rust/codegen/src/json_codec.rs +1 -1
  49. data/rust/codegen/src/main.rs +3 -4
  50. data/rust/codegen/src/mutations.rs +1 -1
  51. data/rust/codegen/src/naming.rs +1 -1
  52. data/rust/codegen/src/ports.rs +1 -1
  53. data/rust/codegen/src/queries.rs +3 -3
  54. data/rust/codegen/src/reactions.rs +1 -1
  55. data/rust/codegen/src/read_models.rs +2 -2
  56. data/rust/codegen/src/reference_specs.rs +1 -1
  57. data/rust/codegen/src/registry.rs +4 -4
  58. data/rust/codegen/src/shared.rs +1 -1
  59. data/rust/codegen/src/skip_reason.rs +1 -1
  60. data/rust/codegen/src/types.rs +4 -4
  61. data/rust/host/HECKS_RELEASE +1 -1
  62. data/rust/host/src/needs.rs +94 -27
  63. data/rust/src/exemplar/commands.rs +1 -1
  64. data/rust/src/exemplar/constraints.rs +1 -1
  65. data/rust/src/exemplar/fielded.rs +1 -1
  66. data/rust/src/exemplar/json.rs +1 -1
  67. data/rust/src/exemplar/mod.rs +1 -1
  68. data/rust/src/exemplar/mutations.rs +1 -1
  69. data/rust/src/exemplar/queries.rs +1 -1
  70. data/rust/src/exemplar/reactions.rs +1 -1
  71. data/rust/src/exemplar/read_models.rs +1 -1
  72. data/rust/src/exemplar/registry.rs +1 -1
  73. data/rust/src/exemplar/types.rs +1 -1
  74. data/rust/src/kernel/cli.rs +2 -2
  75. data/rust/src/kernel/read_model.rs +1 -1
  76. data/rust/src/lib.rs +1 -1
  77. metadata +3 -23
  78. data/rust/project/bridging.rb +0 -257
  79. data/rust/project/commands.rb +0 -877
  80. data/rust/project/constraints.rb +0 -86
  81. data/rust/project/dependency_planning.rb +0 -178
  82. data/rust/project/domain_generator.rb +0 -983
  83. data/rust/project/exemplar.rb +0 -148
  84. data/rust/project/expr_emitter.rb +0 -72
  85. data/rust/project/fielded.rb +0 -141
  86. data/rust/project/json_codec.rb +0 -641
  87. data/rust/project/mutations.rb +0 -695
  88. data/rust/project/naming.rb +0 -223
  89. data/rust/project/ports.rb +0 -65
  90. data/rust/project/queries.rb +0 -493
  91. data/rust/project/reactions.rb +0 -317
  92. data/rust/project/read_models.rb +0 -690
  93. data/rust/project/reference_specs.rb +0 -27
  94. data/rust/project/registry.rb +0 -396
  95. data/rust/project/skip_reason.rb +0 -29
  96. data/rust/project/types.rb +0 -225
  97. data/rust/project/write_if_changed.rb +0 -7
  98. data/rust/project.rb +0 -30
  99. data/rust/project_rust_pipeline.rb +0 -391
@@ -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.2.0
1
+ 3.2.1
@@ -110,21 +110,34 @@ pub fn table(domain_ir: &Value) -> Value {
110
110
  Value::Object(verbs)
111
111
  }
112
112
 
113
+ fn list<'a>(value: &'a Value, key: &str) -> &'a [Value] {
114
+ value.get(key).and_then(Value::as_array).map(Vec::as_slice).unwrap_or(&[])
115
+ }
116
+
117
+ fn named(value: &Value, name: &str) -> bool {
118
+ value.get("name").and_then(Value::as_str) == Some(name)
119
+ }
120
+
121
+ /// The command a verb names: `Domain::Aggregate.Command` on the aggregate itself, or
122
+ /// `Domain::Aggregate.Entity[.Entity].Command` on an entity nested inside it.
123
+ fn command_for<'a>(domain_ir: &'a Value, verb: &str) -> Option<&'a Value> {
124
+ let mut segments = verb.split('.');
125
+ let aggregate = segments.next()?.rsplit("::").next()?;
126
+ let mut path: Vec<&str> = segments.collect();
127
+ let command_name = path.pop()?;
128
+ let mut node = list(domain_ir, "aggregates").iter().find(|a| named(a, aggregate))?;
129
+ for entity in path {
130
+ node = list(node, "entities").iter().find(|e| named(e, entity))?;
131
+ }
132
+ list(node, "commands").iter().find(|c| named(c, command_name))
133
+ }
134
+
113
135
  /// The command's attributes that declare a default (`attribute :runs, Count, default: 30`), each
114
136
  /// with the declared value. An attribute whose default is null declares none.
115
137
  fn declared_defaults<'a>(domain_ir: &'a Value, verb: &str) -> Vec<(&'a str, &'a Value)> {
116
- let Some((qualified_aggregate, command_name)) = verb.rsplit_once('.') else { return Vec::new() };
117
- let aggregate = qualified_aggregate.rsplit("::").next().unwrap_or(qualified_aggregate);
118
- let named = |value: &Value, name: &str| value.get("name").and_then(Value::as_str) == Some(name);
119
- let list = |value: &'a Value, key: &str| -> Vec<&'a Value> {
120
- value.get(key).and_then(Value::as_array).map(|a| a.iter().collect()).unwrap_or_default()
121
- };
122
- list(domain_ir, "aggregates")
123
- .into_iter()
124
- .filter(|a| named(a, aggregate))
125
- .flat_map(|a| list(a, "commands"))
126
- .filter(|c| named(c, command_name))
127
- .flat_map(|c| list(c, "attributes"))
138
+ let Some(command) = command_for(domain_ir, verb) else { return Vec::new() };
139
+ list(command, "attributes")
140
+ .iter()
128
141
  .filter_map(|a| {
129
142
  let default = a.get("default").filter(|d| !d.is_null())?;
130
143
  Some((a.get("name").and_then(Value::as_str)?, default))
@@ -133,29 +146,35 @@ fn declared_defaults<'a>(domain_ir: &'a Value, verb: &str) -> Vec<(&'a str, &'a
133
146
  }
134
147
 
135
148
  /// Every command's declared defaults as the kernel's input `"defaults"` reads them:
136
- /// `{ verb: { attribute: value } }`. The kernel fills a reaction's command from this the same way
137
- /// the host fills the outermost one.
149
+ /// `{ verb: { attribute: value } }`, entity commands included. The kernel fills a reaction's
150
+ /// command from this the same way the host fills the outermost one.
138
151
  pub fn defaults_table(domain_ir: &Value) -> Value {
139
152
  let domain = domain_ir.get("name").and_then(Value::as_str).unwrap_or_default();
140
153
  let mut verbs = serde_json::Map::new();
141
- let aggregates = domain_ir.get("aggregates").and_then(Value::as_array).into_iter().flatten();
142
- for aggregate in aggregates {
154
+ for aggregate in list(domain_ir, "aggregates") {
143
155
  let aggregate_name = aggregate.get("name").and_then(Value::as_str).unwrap_or_default();
144
- for command in aggregate.get("commands").and_then(Value::as_array).into_iter().flatten() {
145
- let command_name = command.get("name").and_then(Value::as_str).unwrap_or_default();
146
- let verb = format!("{domain}::{aggregate_name}.{command_name}");
147
- let defaults: serde_json::Map<String, Value> = declared_defaults(domain_ir, &verb)
148
- .into_iter()
149
- .map(|(name, value)| (name.to_string(), value.clone()))
150
- .collect();
151
- if !defaults.is_empty() {
152
- verbs.insert(verb, Value::Object(defaults));
153
- }
154
- }
156
+ collect_defaults(domain_ir, aggregate, &format!("{domain}::{aggregate_name}"), &mut verbs);
155
157
  }
156
158
  Value::Object(verbs)
157
159
  }
158
160
 
161
+ /// Adds the defaults of `node`'s commands, then of each entity nested in it, under `prefix`.
162
+ fn collect_defaults(domain_ir: &Value, node: &Value, prefix: &str, verbs: &mut serde_json::Map<String, Value>) {
163
+ for command in list(node, "commands") {
164
+ let command_name = command.get("name").and_then(Value::as_str).unwrap_or_default();
165
+ let verb = format!("{prefix}.{command_name}");
166
+ let defaults: serde_json::Map<String, Value> =
167
+ declared_defaults(domain_ir, &verb).into_iter().map(|(name, value)| (name.to_string(), value.clone())).collect();
168
+ if !defaults.is_empty() {
169
+ verbs.insert(verb, Value::Object(defaults));
170
+ }
171
+ }
172
+ for entity in list(node, "entities") {
173
+ let entity_name = entity.get("name").and_then(Value::as_str).unwrap_or_default();
174
+ collect_defaults(domain_ir, entity, &format!("{prefix}.{entity_name}"), verbs);
175
+ }
176
+ }
177
+
159
178
  /// The facts of a command invocation: the `with` object when the call carries one, else the
160
179
  /// flat object itself (the kernel's `CommandInvocation` reads both shapes).
161
180
  fn facts_mut(args: &mut Value) -> Option<&mut serde_json::Map<String, Value>> {
@@ -351,4 +370,52 @@ mod tests {
351
370
  assert_eq!(defaults_table(&defaulted_ir()), json!({"Qc::Check.Start": {"runs": 30}}));
352
371
  assert_eq!(defaults_table(&integer_ir()), json!({}));
353
372
  }
373
+
374
+ // A board entity with a defaulted command, and a card nested inside it with another.
375
+ fn nested_ir() -> Value {
376
+ json!({"name": "Np", "aggregates": [{"name": "Workspace", "commands": [], "entities": [
377
+ {"name": "Board", "commands": [
378
+ {"name": "Retitle", "attributes": [
379
+ {"name": "label", "type": "BoardLabel", "default": {"value": "untitled"}}], "needs": []}
380
+ ], "entities": [
381
+ {"name": "Card", "commands": [
382
+ {"name": "Remark", "attributes": [
383
+ {"name": "note", "type": "CardNote", "default": {"text": "none"}}], "needs": []}
384
+ ]}
385
+ ]}
386
+ ]}]})
387
+ }
388
+
389
+ #[test]
390
+ fn an_entity_command_is_filled_from_its_own_declared_default() {
391
+ let mut args = json!({"reference": {"value": "W1"}, "number": {"value": 1}});
392
+ fill_defaults(&nested_ir(), "Np::Workspace.Board.Retitle", &mut args);
393
+ assert_eq!(args["label"], json!({"value": "untitled"}));
394
+ }
395
+
396
+ #[test]
397
+ fn a_command_on_an_entity_nested_in_an_entity_is_filled_too() {
398
+ let mut args = json!({"with": {"reference": {"value": "W1"}}});
399
+ fill_defaults(&nested_ir(), "Np::Workspace.Board.Card.Remark", &mut args);
400
+ assert_eq!(args["with"]["note"], json!({"text": "none"}));
401
+ }
402
+
403
+ #[test]
404
+ fn an_entity_path_that_names_nothing_is_left_untouched() {
405
+ let mut args = json!({"number": 1});
406
+ fill_defaults(&nested_ir(), "Np::Workspace.Shelf.Retitle", &mut args);
407
+ fill_defaults(&nested_ir(), "Np::Workspace.Board.Remark", &mut args);
408
+ assert_eq!(args, json!({"number": 1}));
409
+ }
410
+
411
+ #[test]
412
+ fn the_kernel_table_lists_entity_commands_under_their_full_path() {
413
+ assert_eq!(
414
+ defaults_table(&nested_ir()),
415
+ json!({
416
+ "Np::Workspace.Board.Retitle": {"label": {"value": "untitled"}},
417
+ "Np::Workspace.Board.Card.Remark": {"note": {"text": "none"}}
418
+ })
419
+ );
420
+ }
354
421
  }
@@ -1,4 +1,4 @@
1
- //! Exemplar shapes for rust/project/commands.rb (see mod.rs).
1
+ //! Exemplar shapes for rust/codegen/src/commands.rs (see mod.rs).
2
2
  //!
3
3
  //! `dispatch_fn` proves the outer `pub fn .. -> DispatchResult { .. dispatch(..) .. }` wrapper.
4
4
  #![allow(dead_code, unused_variables)]
@@ -1,4 +1,4 @@
1
- //! Exemplar shapes for rust/project/constraints.rb (see mod.rs).
1
+ //! Exemplar shapes for rust/codegen/src/constraints.rs (see mod.rs).
2
2
  //!
3
3
  //! Both shapes are single-line refusal checks spliced into a `Result`-returning body.
4
4
  #![allow(dead_code, unused_variables)]
@@ -1,4 +1,4 @@
1
- //! Exemplar shapes for rust/project/fielded.rb (see mod.rs).
1
+ //! Exemplar shapes for rust/codegen/src/fielded.rs (see mod.rs).
2
2
  //!
3
3
  //! Each `Fielded::field` arm shape is a standalone leaf so both outer skeletons can reuse it.
4
4
  #![allow(dead_code, unused_variables)]
@@ -1,4 +1,4 @@
1
- // Exemplar shapes for rust/project/json_codec.rb; see mod.rs.
1
+ // Exemplar shapes for rust/codegen/src/json_codec.rs; see mod.rs.
2
2
  // Kept out of the fenced region so its prose is not baked into every generated codec.
3
3
  #![allow(dead_code, unused_variables)]
4
4
 
@@ -1,4 +1,4 @@
1
- // Compile-checked Rust for every shape rust/project/*.rb emits; exemplar.rb slices the fenced
1
+ // Compile-checked Rust for every shape rust/codegen/src/*.rs emits; exemplar.rs slices the fenced
2
2
  // `// TMPL:<id> BEGIN` / `END` regions and substitutes real names for the `tmpl_` vocabulary.
3
3
  //
4
4
  // `tmpl_*_host` functions only give fenced fragments real types and are never called, hence
@@ -1,4 +1,4 @@
1
- // Exemplar shapes for rust/project/mutations.rb; see mod.rs.
1
+ // Exemplar shapes for rust/codegen/src/mutations.rs; see mod.rs.
2
2
  // Hosts take `record: &mut Tmpl...`, not `&mut self`, as the generated mutation closures do.
3
3
  #![allow(dead_code, unused_variables)]
4
4
 
@@ -1,4 +1,4 @@
1
- // Exemplar shape for rust/project/registry.rb's `emit_query_table`; see mod.rs.
1
+ // Exemplar shape for rust/codegen/src/registry.rs's `emit_query_table`; see mod.rs.
2
2
  #![allow(dead_code, unused_variables)]
3
3
 
4
4
  // A `const` cannot call a function, so the row is a real literal substituted wholesale.
@@ -1,4 +1,4 @@
1
- // Exemplar shapes that rust/project/reactions.rb slices into generated tables;
1
+ // Exemplar shapes that rust/codegen/src/reactions.rs slices into generated tables;
2
2
  // see mod.rs for how they are used.
3
3
  #![allow(dead_code, unused_variables)]
4
4
 
@@ -1,4 +1,4 @@
1
- // Exemplar shapes for rust/project/read_models.rb's `emit_read_model_table`
1
+ // Exemplar shapes for rust/codegen/src/read_models.rs's `emit_read_model_table`
2
2
  // — see mod.rs's own header. `ReadModelDef`/`ReadModelHead`/`FilteredHead`/
3
3
  // `ReferenceField` are real kernel types (`rust/src/kernel/read_model.rs`).
4
4
  #![allow(dead_code, unused_variables)]
@@ -1,4 +1,4 @@
1
- // Exemplar shapes for rust/project/registry.rb; see mod.rs.
1
+ // Exemplar shapes for rust/codegen/src/registry.rs; see mod.rs.
2
2
  // `InMemoryRepository<T>` only needs `T: Clone`, so an `i64` record type suffices.
3
3
  #![allow(dead_code, unused_variables)]
4
4
 
@@ -1,4 +1,4 @@
1
- // Exemplar shapes for rust/project/types.rb; see mod.rs.
1
+ // Exemplar shapes for rust/codegen/src/types.rs; see mod.rs.
2
2
  // `closed_set_enum` is a one-field closed set (one variant per member); a multi-field closed
3
3
  // set is a fixed data table instead.
4
4
  #![allow(dead_code, unused_variables)]
@@ -159,7 +159,7 @@ pub fn run(input: &str) -> String {
159
159
  real declared query whose shape this generator's codegen doesn't cover yet (order_by/limit/\
160
160
  cursor/consistency/freshness/authorization/null_semantics/inspection/index_hints, a where \
161
161
  clause hopping through a reference, or a literal comparator value whose true JSON type \
162
- can't be recovered from the exported IR — rust/project/queries.rb's own header has the \
162
+ can't be recovered from the exported IR — rust/codegen/src/queries.rs's own header has the \
163
163
  full argument); the wheres-only, single-aggregate field-comparator subset and the ad hoc \
164
164
  filter shape ({{\"aggregate\",\"field\",\"op\",\"value\"}}) both execute for real"
165
165
  )),
@@ -195,7 +195,7 @@ pub fn run(input: &str) -> String {
195
195
  or a real declared read model whose shape this generator's codegen doesn't cover yet \
196
196
  (anything beyond a root aggregate fetched by reference id plus reference-matched sibling \
197
197
  heads — where/order_by/limit/offset/cursor/consistency/freshness/authorize(TenantScope)/\
198
- nulls/inspect_query/use_index — rust/project/read_models.rb's own header has the full \
198
+ nulls/inspect_query/use_index — rust/codegen/src/read_models.rs's own header has the full \
199
199
  argument, including why where/order_by/limit specifically can never be recovered from the \
200
200
  canonical IR at all); the named/declared AGGREGATE query form (\"Domain::Aggregate.Query\") \
201
201
  and the ad hoc filter shape ({{\"aggregate\",\"field\",\"op\",\"value\"}}) both execute for \
@@ -1,5 +1,5 @@
1
1
  //! Interprets a compiled `report` block (`ReadModelDef`) against a domain's generated
2
- //! `READ_MODELS` table — the hand-written counterpart to `rust/project/read_models.rb`.
2
+ //! `READ_MODELS` table — the hand-written counterpart to the retired Ruby generator's `read_models.rb`.
3
3
 
4
4
  use super::refusal_wording::{InvariantViolationGroupByCollisionArgs, NotFoundReadModelReferenceMissingArgs, UnauthorizedTenantRequiredArgs};
5
5
  use super::{named_query, query_comparators, query_ordering, repository, AggregateScan, Json, QueryCondition, QueryConditionValue, Refusal};
data/rust/src/lib.rs CHANGED
@@ -3,6 +3,6 @@
3
3
  pub mod generated;
4
4
  pub mod kernel;
5
5
 
6
- // Test-only: rust/project/exemplar.rb slices shapes out of this; never in a release build.
6
+ // Test-only: rust/codegen/src/exemplar.rs slices shapes out of this; never in a release build.
7
7
  #[cfg(test)]
8
8
  pub mod exemplar;
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hecks
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young
@@ -273,6 +273,7 @@ files:
273
273
  - lib/hecks/doors/launcher_options.rb
274
274
  - lib/hecks/doors/mcp_door.rb
275
275
  - lib/hecks/doors/mcp_door_scope.rb
276
+ - lib/hecks/doors/mcp_guide_cache.rb
276
277
  - lib/hecks/doors/ruby_door.rb
277
278
  - lib/hecks/doors/ruby_door/aggregate_door.rb
278
279
  - lib/hecks/doors/ruby_door/chapter.rb
@@ -616,6 +617,7 @@ files:
616
617
  - lib/hecks/projections/site/site_cdn.rb
617
618
  - lib/hecks/projections/site/table.rb
618
619
  - lib/hecks/projections/site/table/checks.rb
620
+ - lib/hecks/projections/site/table/link_builder.rb
619
621
  - lib/hecks/projections/site/table/row_builder.rb
620
622
  - lib/hecks/projections/statements.rb
621
623
  - lib/hecks/projections/vocabulary.rb
@@ -973,28 +975,6 @@ files:
973
975
  - rust/parser/src/parse/read_model.rs
974
976
  - rust/parser/src/parse/value_object.rs
975
977
  - rust/parser/src/ruby_value.rs
976
- - rust/project.rb
977
- - rust/project/bridging.rb
978
- - rust/project/commands.rb
979
- - rust/project/constraints.rb
980
- - rust/project/dependency_planning.rb
981
- - rust/project/domain_generator.rb
982
- - rust/project/exemplar.rb
983
- - rust/project/expr_emitter.rb
984
- - rust/project/fielded.rb
985
- - rust/project/json_codec.rb
986
- - rust/project/mutations.rb
987
- - rust/project/naming.rb
988
- - rust/project/ports.rb
989
- - rust/project/queries.rb
990
- - rust/project/reactions.rb
991
- - rust/project/read_models.rb
992
- - rust/project/reference_specs.rb
993
- - rust/project/registry.rb
994
- - rust/project/skip_reason.rb
995
- - rust/project/types.rb
996
- - rust/project/write_if_changed.rb
997
- - rust/project_rust_pipeline.rb
998
978
  - rust/src/exemplar/commands.rs
999
979
  - rust/src/exemplar/constraints.rs
1000
980
  - rust/src/exemplar/fielded.rs