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
@@ -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
  }
data/rust/host/src/web.rs CHANGED
@@ -369,7 +369,10 @@ async fn active_session_email(
369
369
 
370
370
  async fn accounts_me_route(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &str, client: &Mutex<Client>) -> Value {
371
371
  match active_session_email(domain_ir, cookies, secret, client).await {
372
- Ok(email) => respond(200, "application/json", &json!({"email": email}).to_string()),
372
+ Ok(email) => match auth::member_identity_id(client, domain_ir, &email).await {
373
+ Ok(identity_id) => respond(200, "application/json", &json!({"email": email, "identity_id": identity_id}).to_string()),
374
+ Err(e) => respond(500, "application/json", &json!({"error": format!("members lookup failed: {e}")}).to_string()),
375
+ },
373
376
  Err(response) => response,
374
377
  }
375
378
  }
@@ -434,7 +437,7 @@ pub(crate) async fn add_member_route(
434
437
  Ok(true) => respond(
435
438
  201,
436
439
  "application/json",
437
- &json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false}).to_string(),
440
+ &json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false, "identity_id": null}).to_string(),
438
441
  ),
439
442
  Ok(false) => json_error(500, &format!("the person was admitted but the {role} role was not granted")),
440
443
  Err(e) => json_error(500, &format!("the person was admitted but the {role} role was not granted: {e}")),
@@ -2173,6 +2176,7 @@ mod tests {
2173
2176
  assert_eq!(response["statusCode"], 200, "{response:?}");
2174
2177
  let body: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
2175
2178
  assert_eq!(body["email"], "zed@example.com");
2179
+ assert_eq!(body["identity_id"], "id-1", "the signed-in account's Governance identity id");
2176
2180
 
2177
2181
  let empty = HashMap::new();
2178
2182
  assert_eq!(accounts_me_route(&domain_ir, &empty, secret, &client).await["statusCode"], 401);
@@ -2273,11 +2277,11 @@ mod tests {
2273
2277
  assert_eq!(names, ["amy", "Zed"], "sorted case-insensitively by name");
2274
2278
  assert_eq!(
2275
2279
  people[1],
2276
- json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false})
2280
+ json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false, "identity_id": "id-1"})
2277
2281
  );
2278
2282
  assert_eq!(
2279
2283
  people[0],
2280
- json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false})
2284
+ json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false, "identity_id": null})
2281
2285
  );
2282
2286
  }
2283
2287
 
@@ -2465,7 +2469,7 @@ mod tests {
2465
2469
  let created: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
2466
2470
  assert_eq!(
2467
2471
  created,
2468
- json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false})
2472
+ json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false, "identity_id": null})
2469
2473
  );
2470
2474
 
2471
2475
  let listing = members_route(&domain_ir, &cookies, secret, &client).await;
@@ -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.1.3
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
@@ -380,6 +381,8 @@ files:
380
381
  - lib/hecks/hecks/adapters/codebase/test_suite.rb
381
382
  - lib/hecks/hecks/adapters/codebase/tree.rb
382
383
  - lib/hecks/hecks/adapters/console_capture.rb
384
+ - lib/hecks/hecks/adapters/cost_explorer.adapter
385
+ - lib/hecks/hecks/adapters/cost_explorer.rb
383
386
  - lib/hecks/hecks/adapters/deploy_toolchain.adapter
384
387
  - lib/hecks/hecks/adapters/deploy_toolchain.rb
385
388
  - lib/hecks/hecks/adapters/finding_github.adapter
@@ -614,6 +617,7 @@ files:
614
617
  - lib/hecks/projections/site/site_cdn.rb
615
618
  - lib/hecks/projections/site/table.rb
616
619
  - lib/hecks/projections/site/table/checks.rb
620
+ - lib/hecks/projections/site/table/link_builder.rb
617
621
  - lib/hecks/projections/site/table/row_builder.rb
618
622
  - lib/hecks/projections/statements.rb
619
623
  - lib/hecks/projections/vocabulary.rb
@@ -971,28 +975,6 @@ files:
971
975
  - rust/parser/src/parse/read_model.rs
972
976
  - rust/parser/src/parse/value_object.rs
973
977
  - rust/parser/src/ruby_value.rs
974
- - rust/project.rb
975
- - rust/project/bridging.rb
976
- - rust/project/commands.rb
977
- - rust/project/constraints.rb
978
- - rust/project/dependency_planning.rb
979
- - rust/project/domain_generator.rb
980
- - rust/project/exemplar.rb
981
- - rust/project/expr_emitter.rb
982
- - rust/project/fielded.rb
983
- - rust/project/json_codec.rb
984
- - rust/project/mutations.rb
985
- - rust/project/naming.rb
986
- - rust/project/ports.rb
987
- - rust/project/queries.rb
988
- - rust/project/reactions.rb
989
- - rust/project/read_models.rb
990
- - rust/project/reference_specs.rb
991
- - rust/project/registry.rb
992
- - rust/project/skip_reason.rb
993
- - rust/project/types.rb
994
- - rust/project/write_if_changed.rb
995
- - rust/project_rust_pipeline.rb
996
978
  - rust/src/exemplar/commands.rs
997
979
  - rust/src/exemplar/constraints.rs
998
980
  - rust/src/exemplar/fielded.rs
@@ -1,257 +0,0 @@
1
- module RustProjection
2
- module Projector
3
- module_function
4
-
5
- # `use` statements for value-object types this aggregate's commands
6
- # reference by name but don't declare locally (same chapter only).
7
- def cross_aggregate_vo_imports(aggregate, domain_value_object_owner, mod_name)
8
- local_names = aggregate[:value_objects].map { |vo| vo[:name] }.to_set
9
- attrs = (aggregate[:commands] + aggregate[:entities].flat_map { |e| e[:commands] } +
10
- aggregate[:ports].flat_map { |p| p[:operations] })
11
- .flat_map { |c| c[:attributes] }
12
-
13
- # A locally-declared type is never "foreign", even if some other
14
- # aggregate owns a VO of the same name domain-wide.
15
- foreign_types = attrs.map { |a| a[:type] }.uniq.reject { |type| local_names.include?(type) }
16
- foreign_types.filter_map { |type| domain_value_object_owner[type] && [type, domain_value_object_owner[type]] }
17
- .sort_by { |(type, owner)| [owner, type] }
18
- .map { |(type, owner)| "use crate::generated::#{mod_name}::#{owner.downcase}::#{rust_ident(type)};" }
19
- end
20
-
21
- # Whether target_vo's fields can all be filled from source_vo's fields
22
- # (matched by name) or the target's own defaults.
23
- def vo_field_bridgeable?(source_vo, target_vo)
24
- return false unless source_vo && target_vo
25
- return closed_sets_bridgeable?(source_vo, target_vo) if source_vo[:closed_set] || target_vo[:closed_set]
26
-
27
- target_vo[:attributes].all? do |t_attr|
28
- source_vo[:attributes].any? { |s_attr| s_attr[:name] == t_attr[:name] } || !t_attr[:default].nil?
29
- end
30
- end
31
-
32
- # Source closed-set members mappable to target members, or nil unless
33
- # every source member is also a target member (so the bridge can't fail).
34
- def closed_set_bridge_members(source_vo, target_vo)
35
- return nil unless source_vo[:closed_set] && target_vo[:closed_set]
36
- return nil unless source_vo[:attributes].size == 1 && target_vo[:attributes].size == 1
37
-
38
- target_values = target_vo[:members].map { |row| row.to_h.values.first.to_s }
39
- source_rows = source_vo[:members]
40
- return nil unless source_rows.all? { |row| target_values.include?(row.to_h.values.first.to_s) }
41
-
42
- source_rows
43
- end
44
-
45
- def closed_sets_bridgeable?(source_vo, target_vo)
46
- !closed_set_bridge_members(source_vo, target_vo).nil?
47
- end
48
-
49
- def vo_field_rhs(source_expr, source_vo, target_type, value_objects_by_name)
50
- target_vo = value_objects_by_name[target_type]
51
- if (rows = closed_set_bridge_members(source_vo, target_vo))
52
- arms = rows.map { |row| "#{rust_ident(source_vo[:name])}::#{closed_set_variant(row)} => #{rust_ident(target_type)}::#{closed_set_variant(row)}" }
53
- return "match &#{source_expr} { #{arms.join(', ')} }"
54
- end
55
-
56
- fields = target_vo[:attributes].map do |t_attr|
57
- field = rust_ident_field(t_attr[:name])
58
- if source_vo[:attributes].any? { |s_attr| s_attr[:name] == t_attr[:name] }
59
- "#{field}: #{source_expr}.#{field}.clone()"
60
- else
61
- "#{field}: #{literal_rhs(t_attr[:default])}"
62
- end
63
- end
64
- "#{rust_ident(target_type)} { #{fields.join(', ')} }"
65
- end
66
-
67
- # Whether a value can move from source_type to target_type. Shared
68
- # with `value_rhs`, which performs the bridge this checks for.
69
- def bridgeable_value_types?(source_type, target_type, value_objects_by_name)
70
- return true if source_type == target_type
71
- # Compares Rust representations, not declared type names: a
72
- # `Reference<T>` and a bare `String` are both just `String`.
73
- return true if effective_scalar_type(source_type) && effective_scalar_type(source_type) == effective_scalar_type(target_type)
74
-
75
- source_vo = value_objects_by_name[source_type]
76
- target_vo = value_objects_by_name[target_type]
77
- return vo_field_bridgeable?(source_vo, target_vo) if target_vo
78
-
79
- # A single-field source VO unwraps into a bare scalar target when
80
- # that one field's own Rust representation already matches it.
81
- return false unless source_vo && !source_vo[:closed_set] && source_vo[:attributes].size == 1
82
-
83
- unwrapped_type = source_vo[:attributes].first[:type]
84
- unwrapped_type == target_type ||
85
- (effective_scalar_type(unwrapped_type) && effective_scalar_type(unwrapped_type) == effective_scalar_type(target_type))
86
- end
87
-
88
- def value_rhs(source_expr, source_type, target_type, value_objects_by_name)
89
- return "#{source_expr}.clone()" if source_type == target_type
90
- return "#{source_expr}.clone()" if effective_scalar_type(source_type) && effective_scalar_type(source_type) == effective_scalar_type(target_type)
91
-
92
- source_vo = value_objects_by_name[source_type]
93
- target_vo = value_objects_by_name[target_type]
94
- return vo_field_rhs(source_expr, source_vo, target_type, value_objects_by_name) if target_vo
95
-
96
- raise "unsupported coercion #{source_type} -> #{target_type} — bridgeable_value_types? should have caught this" unless source_vo && !source_vo[:closed_set] && source_vo[:attributes].size == 1
97
-
98
- "#{source_expr}.#{rust_ident_field(source_vo[:attributes].first[:name])}.clone()"
99
- end
100
-
101
- # Whether a list `:set` needs per-element rebuilding, rather than one
102
- # `.clone()`, because source and target elements differ in representation.
103
- def list_bridge_requires_element_mapping?(source_type, target_type)
104
- return false if source_type == target_type
105
-
106
- !(effective_scalar_type(source_type) && effective_scalar_type(source_type) == effective_scalar_type(target_type))
107
- end
108
-
109
- # `value_rhs`, mapped over each element of a list `:set` whose source
110
- # and target element types differ.
111
- def list_value_rhs(source_expr, source_type, target_type, value_objects_by_name)
112
- "#{source_expr}.iter().map(|item| #{value_rhs('item', source_type, target_type, value_objects_by_name)}).collect()"
113
- end
114
-
115
- def literal_set_bridgeable?(value, target_type, value_objects_by_name)
116
- return literal_hash_bridgeable?(value, target_type, value_objects_by_name) if value.is_a?(Hash) && target_type
117
- return false if value.is_a?(Hash)
118
- return false unless value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
119
- return true unless target_type && value_objects_by_name[target_type]
120
-
121
- # A scalar literal into a single-field VO stands for that field's
122
- # value (mirrors `Value.for_attribute`'s runtime rewrap).
123
- sole = sole_field_of(target_type, value_objects_by_name)
124
- return false unless sole
125
-
126
- literal_hash_bridgeable?({ sole => value }, target_type, value_objects_by_name)
127
- end
128
-
129
- # The right-hand side for a literal into target_type: a Hash, a
130
- # scalar rewrapped into a single-field VO, or the scalar itself.
131
- def literal_rhs_for(value, target_type, value_objects_by_name)
132
- return literal_hash_rhs(value, target_type, value_objects_by_name) if value.is_a?(Hash)
133
-
134
- sole = target_type && value_objects_by_name[target_type] && sole_field_of(target_type, value_objects_by_name)
135
- return literal_hash_rhs({ sole => value }, target_type, value_objects_by_name) if sole
136
-
137
- literal_rhs(value)
138
- end
139
-
140
- # Whether a literal Hash supplies every field a target VO (or one
141
- # of its closed-set members) needs.
142
- def literal_hash_bridgeable?(hash, target_type, value_objects_by_name)
143
- vo = value_objects_by_name[target_type]
144
- return false unless vo
145
-
146
- if vo[:closed_set]
147
- vo[:members].any? { |member| member.all? { |field, value| [hash[field.to_sym], hash[field.to_s]].include?(value) } }
148
- else
149
- vo[:attributes].all? { |attr| hash.key?(attr[:name].to_sym) || hash.key?(attr[:name].to_s) }
150
- end
151
- end
152
-
153
- def literal_hash_rhs(hash, target_type, value_objects_by_name)
154
- vo = value_objects_by_name[target_type]
155
- raise "unsupported literal hash source #{hash.inspect} -> #{target_type} — literal_hash_bridgeable? should have caught this" unless vo
156
-
157
- if vo[:closed_set]
158
- row = vo[:members].find { |member| member.all? { |field, value| [hash[field.to_sym], hash[field.to_s]].include?(value) } }
159
- raise "literal #{hash.inspect} matches no member of #{target_type} — literal_hash_bridgeable? should have caught this" unless row
160
-
161
- "#{rust_ident(target_type)}::#{closed_set_variant(row)}"
162
- else
163
- fields = vo[:attributes].map do |attr|
164
- key = [attr[:name].to_sym, attr[:name].to_s].find { |k| hash.key?(k) }
165
- raise "literal #{hash.inspect} missing field #{attr[:name]} for #{target_type} — literal_hash_bridgeable? should have caught this" unless key
166
-
167
- "#{rust_ident_field(attr[:name])}: #{literal_rhs(hash[key])}"
168
- end
169
- "#{rust_ident(target_type)} { #{fields.join(', ')} }"
170
- end
171
- end
172
-
173
- def integer_field_of(vo)
174
- return nil unless vo && !vo[:closed_set]
175
-
176
- attr = vo[:attributes].find { |a| a[:type] == "Integer" }
177
- attr && attr[:name]
178
- end
179
-
180
- # The attribute and its one Integer field an `:increment`/`:decrement`
181
- # touches, or nil unless the target is a single-field integer VO.
182
- def arithmetic_target_field(mutation, aggregate, value_objects_by_name)
183
- target_attr = aggregate[:attributes].find { |a| a[:name].to_s == mutation[:target].to_s }
184
- return nil unless target_attr && !target_attr[:list]
185
-
186
- field = integer_field_of(value_objects_by_name[target_attr[:type]])
187
- field && [target_attr, field]
188
- end
189
-
190
- # The amount side of an `:increment`/`:decrement` as a raw Rust
191
- # integer expression. Returns nil, not raise, when nothing bridges.
192
- def arithmetic_amount_expr(source, command, value_objects_by_name, target_integer_field)
193
- if source[:kind] == "literal"
194
- value = source[:value]
195
- return literal_rhs(value) if value.is_a?(Integer)
196
- return nil unless value.is_a?(Hash)
197
-
198
- key = [target_integer_field.to_sym, target_integer_field.to_s].find { |k| value.key?(k) }
199
- key && value[key].is_a?(Integer) ? literal_rhs(value[key]) : nil
200
- elsif source[:kind] == "argument"
201
- arg_attr = command[:attributes].find { |a| a[:name].to_s == source[:name] }
202
- return nil unless arg_attr
203
- return "args.#{rust_ident_field(arg_attr[:name])}" if arg_attr[:type] == "Integer"
204
-
205
- field = integer_field_of(value_objects_by_name[arg_attr[:type]])
206
- field && "args.#{rust_ident_field(arg_attr[:name])}.#{rust_ident_field(field)}"
207
- end
208
- end
209
-
210
- # `clamp:`'s literal `[min, max]` bounds, or nil for anything else
211
- # (non-literal source, wrong length, non-Integer bound).
212
- def clamp_bounds_ints(source)
213
- return nil unless source[:kind] == "literal"
214
-
215
- value = source[:value]
216
- return nil unless value.is_a?(Array) && value.size == 2 && value.all? { |v| v.is_a?(Integer) }
217
-
218
- value
219
- end
220
-
221
- # The default for an attribute a creating command's arguments don't
222
- # mention: its own default, or one built from its VO's field defaults.
223
- def creation_default_rhs(attr, value_objects_by_name)
224
- default = attr[:default]
225
- unless default.nil?
226
- return literal_rhs(default) unless default.is_a?(Hash)
227
-
228
- # Completes a partial Hash default with the target VO's own
229
- # per-field defaults first; `literal_hash_rhs` requires every
230
- # field present.
231
- return literal_hash_rhs(complete_hash_default(default, attr[:type], value_objects_by_name), attr[:type], value_objects_by_name)
232
- end
233
-
234
- vo = value_objects_by_name[attr[:type]]
235
- return nil unless vo && !vo[:closed_set] && vo[:attributes].all? { |f| !f[:default].nil? }
236
-
237
- fields = vo[:attributes].map { |f| "#{rust_ident_field(f[:name])}: #{literal_rhs(f[:default])}" }
238
- "#{rust_ident(attr[:type])} { #{fields.join(', ')} }"
239
- end
240
-
241
- # Fields present in `hash`, plus any the target VO defaults on its
242
- # own; a field with neither is left out, not set to nil.
243
- def complete_hash_default(hash, target_type, value_objects_by_name)
244
- vo = value_objects_by_name[target_type]
245
- return hash unless vo && !vo[:closed_set]
246
-
247
- vo[:attributes].each_with_object({}) do |field, completed|
248
- key = [field[:name].to_sym, field[:name].to_s].find { |k| hash.key?(k) }
249
- if key
250
- completed[field[:name].to_sym] = hash[key]
251
- elsif !field[:default].nil?
252
- completed[field[:name].to_sym] = field[:default]
253
- end
254
- end
255
- end
256
- end
257
- end