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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
- data/lib/hecks/deploy/oidc.json +15 -0
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +43 -4
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/cost_explorer.adapter +7 -0
- data/lib/hecks/hecks/adapters/cost_explorer.rb +102 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +21 -11
- data/lib/hecks/hecks/hecks.hecksagon +8 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +7 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/settings.rb +23 -2
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -1
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box.rb +13 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/rust_build/kernel_input.rb +18 -6
- data/lib/hecks/rust_build/project_rust.rb +6 -132
- data/lib/hecks/site/bluebook/site.bluebook +27 -13
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/deploy_recipe.rb +1 -0
- data/lib/hecks/tools/rust_comment_style.rb +1 -1
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -1
- data/lib/hecks.rb +1 -0
- data/rust/build/src/lineage_pass.rs +1 -1
- data/rust/build/src/pipeline.rs +1 -1
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +1 -1
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/main.rs +3 -4
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +1 -1
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/auth.rs +25 -1
- data/rust/host/src/needs.rs +94 -27
- data/rust/host/src/web.rs +9 -5
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +2 -2
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +5 -23
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- data/rust/project_rust_pipeline.rb +0 -391
data/rust/host/src/needs.rs
CHANGED
|
@@ -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(
|
|
117
|
-
|
|
118
|
-
|
|
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 } }
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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/
|
|
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/
|
|
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/
|
|
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)]
|
data/rust/src/exemplar/json.rs
CHANGED
data/rust/src/exemplar/mod.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Compile-checked Rust for every shape rust/
|
|
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 shape for rust/
|
|
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 for rust/
|
|
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)]
|
data/rust/src/exemplar/types.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Exemplar shapes for rust/
|
|
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)]
|
data/rust/src/kernel/cli.rs
CHANGED
|
@@ -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/
|
|
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/
|
|
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 `
|
|
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
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
|
|
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
|
data/rust/project/bridging.rb
DELETED
|
@@ -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
|