hecks 3.1.0 → 3.1.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +1 -1
  3. data/lib/hecks/chapters.rb +4 -3
  4. data/lib/hecks/cli/domain_writer.rb +39 -0
  5. data/lib/hecks/cli/interview_agent.rb +59 -0
  6. data/lib/hecks/cli/interview_run.rb +87 -0
  7. data/lib/hecks/cli/interview_session.rb +299 -0
  8. data/lib/hecks/corpus.rb +1 -0
  9. data/lib/hecks/doors/cli_runner.rb +1 -1
  10. data/lib/hecks/doors/usage_cache.rb +123 -0
  11. data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
  12. data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
  13. data/lib/hecks/hecks/adapters/terminal.rb +15 -0
  14. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  15. data/lib/hecks/hecks/custodian.bluebook +36 -0
  16. data/lib/hecks/hecks/hecks.bluebook +18 -0
  17. data/lib/hecks/hecks/hecks.hecksagon +8 -2
  18. data/lib/hecks/hecks/hecks.world +3 -2
  19. data/lib/hecks/projections/deploy/box/settings.rb +87 -4
  20. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +2 -0
  21. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +18 -5
  22. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +17 -1
  23. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
  24. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +1 -1
  25. data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
  26. data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
  27. data/lib/hecks/projections/deploy/box.rb +216 -20
  28. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +20 -4
  29. data/lib/hecks/projections/deploy/scripts/settings.rb +6 -0
  30. data/lib/hecks/projector/cli_projector.rb +38 -14
  31. data/lib/hecks/runtime/loader.rb +31 -8
  32. data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
  33. data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
  34. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
  35. data/lib/hecks/version.rb +1 -1
  36. data/lib/hecks.rb +1 -0
  37. data/rust/codegen/src/commands.rs +4 -32
  38. data/rust/codegen/src/json_codec.rs +3 -0
  39. data/rust/codegen/src/mutations.rs +74 -5
  40. data/rust/codegen/src/naming.rs +4 -0
  41. data/rust/codegen/src/types.rs +45 -0
  42. data/rust/host/HECKS_RELEASE +1 -1
  43. data/rust/host/src/web/registration_receipt/tests.rs +39 -0
  44. data/rust/host/src/web/registration_receipt.rs +122 -0
  45. data/rust/host/src/web/registrations.rs +14 -2
  46. data/rust/host/src/web.rs +1 -0
  47. metadata +17 -2
@@ -392,6 +392,23 @@ pub struct Transition {
392
392
  pub field: String,
393
393
  pub to_state: String,
394
394
  pub from_states: Vec<String>,
395
+ /// A row with no `from:` admits every state, so no check is emitted for the command.
396
+ pub unconstrained: bool,
397
+ }
398
+
399
+ /// The `Option<TransitionCheck>` expression a dispatch function is generated with.
400
+ ///
401
+ /// `None` covers both "no transition" and an unconstrained one; an empty `from_states` slice
402
+ /// would refuse every state instead of admitting every state.
403
+ pub fn transition_check_arg(transition: Option<&Transition>) -> String {
404
+ match transition {
405
+ Some(t) if !t.unconstrained => format!(
406
+ "Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
407
+ naming::ruby_inspect_string(&t.field),
408
+ t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
409
+ ),
410
+ _ => "None".to_string(),
411
+ }
395
412
  }
396
413
 
397
414
  /// Collapses a command's transition rows into the shape `TransitionCheck` wants.
@@ -417,19 +434,25 @@ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Tran
417
434
  }
418
435
  }
419
436
  let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("").to_string();
420
- return Some(Transition { field, to_state: String::new(), from_states });
437
+ return Some(Transition { field, to_state: String::new(), from_states, unconstrained: false });
421
438
  }
422
439
 
423
440
  let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("").to_string();
424
441
  let to_state = rows[0].get("to_state").map(Json::to_s).unwrap_or_default();
425
442
  let mut from_states: Vec<String> = Vec::new();
443
+ let mut unconstrained = false;
426
444
  for row in &rows {
427
- let from = row.get("from_state").map(Json::to_s).unwrap_or_default();
428
- if !from_states.contains(&from) {
429
- from_states.push(from);
445
+ match row.get_raw("from_state") {
446
+ None | Some(Json::Null) => unconstrained = true,
447
+ Some(from) => {
448
+ let from = from.to_s();
449
+ if !from_states.contains(&from) {
450
+ from_states.push(from);
451
+ }
452
+ }
430
453
  }
431
454
  }
432
- Some(Transition { field, to_state, from_states })
455
+ Some(Transition { field, to_state, from_states, unconstrained })
433
456
  }
434
457
 
435
458
  /// Right-hand side for a `set`, coercing the source into the target attribute's declared type.
@@ -782,3 +805,49 @@ fn emit_mutation_line_body(
782
805
  other => panic!("unsupported mutation op {other:?} — command_skip_reason should have caught this"),
783
806
  }
784
807
  }
808
+
809
+ #[cfg(test)]
810
+ mod transition_tests {
811
+ use super::*;
812
+
813
+ fn aggregate(rows: &str) -> Json {
814
+ Json::parse(&format!(
815
+ r#"{{"name":"Account","lifecycle":{{"field":"status","default":"draft","transitions":[{rows}]}}}}"#
816
+ ))
817
+ .unwrap()
818
+ }
819
+
820
+ fn command(name: &str) -> Json {
821
+ Json::parse(&format!(r#"{{"name":"{name}"}}"#)).unwrap()
822
+ }
823
+
824
+ #[test]
825
+ fn a_transition_without_from_admits_every_state() {
826
+ let account = aggregate(r#"{"command":"Open","to_state":"open","from_state":null}"#);
827
+ let transition = lifecycle_transition_for(&command("Open"), &account).unwrap();
828
+ assert!(transition.unconstrained);
829
+ assert_eq!(transition.to_state, "open");
830
+ assert_eq!(transition_check_arg(Some(&transition)), "None");
831
+ }
832
+
833
+ #[test]
834
+ fn a_transition_with_from_checks_that_state() {
835
+ let account = aggregate(r#"{"command":"Close","to_state":"closed","from_state":"open"}"#);
836
+ let transition = lifecycle_transition_for(&command("Close"), &account).unwrap();
837
+ assert!(!transition.unconstrained);
838
+ assert_eq!(
839
+ transition_check_arg(Some(&transition)),
840
+ r#"Some(crate::kernel::TransitionCheck { field: "status", from_states: &["open"] })"#
841
+ );
842
+ }
843
+
844
+ #[test]
845
+ fn one_unconstrained_row_among_constrained_ones_admits_every_state() {
846
+ let account = aggregate(
847
+ r#"{"command":"Reopen","to_state":"open","from_state":"closed"},
848
+ {"command":"Reopen","to_state":"open","from_state":null}"#,
849
+ );
850
+ let transition = lifecycle_transition_for(&command("Reopen"), &account).unwrap();
851
+ assert_eq!(transition_check_arg(Some(&transition)), "None");
852
+ }
853
+ }
@@ -7,6 +7,7 @@ pub fn scalar_rust_type(type_name: &str) -> Option<&'static str> {
7
7
  "String" => Some("String"),
8
8
  "Integer" => Some("i64"),
9
9
  "Float" => Some("f64"),
10
+ "TrueClass" | "FalseClass" => Some("bool"),
10
11
  _ => None,
11
12
  }
12
13
  }
@@ -29,6 +30,8 @@ pub fn effective_scalar_type(type_name: &str) -> Option<&'static str> {
29
30
  "String" => Some("String"),
30
31
  "Integer" => Some("Integer"),
31
32
  "Float" => Some("Float"),
33
+ "TrueClass" => Some("TrueClass"),
34
+ "FalseClass" => Some("FalseClass"),
32
35
  _ => None,
33
36
  }
34
37
  }
@@ -250,6 +253,7 @@ pub fn scalar_to_value(type_name: &str, rust_expr: &str) -> Option<String> {
250
253
  "String" => Some(format!("Value::Str({rust_expr}.clone())")),
251
254
  "Integer" => Some(format!("Value::Int({rust_expr})")),
252
255
  "Float" => Some(format!("Value::Float({rust_expr})")),
256
+ "TrueClass" | "FalseClass" => Some(format!("Value::Bool({rust_expr})")),
253
257
  _ => None,
254
258
  }
255
259
  }
@@ -360,3 +360,48 @@ pub fn emit_projected_field_table(aggregate: &Json) -> String {
360
360
  .collect();
361
361
  format!("pub static {name}_PROJECTED_FIELDS: &[crate::kernel::ProjectedFieldSpec] = &[\n{}\n];\n", rows.join("\n"))
362
362
  }
363
+
364
+ #[cfg(test)]
365
+ mod tests {
366
+ use super::*;
367
+
368
+ /// A value object with one attribute per Ruby spelling of a boolean.
369
+ fn flag_value_object() -> Json {
370
+ Json::parse(
371
+ r#"{
372
+ "name":"Entry",
373
+ "attributes":[
374
+ {"name":"on","type":"TrueClass","list":false,"optional":false,"default":false},
375
+ {"name":"off","type":"FalseClass","list":false,"optional":false}
376
+ ]
377
+ }"#,
378
+ )
379
+ .unwrap()
380
+ }
381
+
382
+ #[test]
383
+ fn boolean_attributes_are_rust_bool_not_a_type_named_for_the_ruby_class() {
384
+ let vo = flag_value_object();
385
+ let generated = emit_value_object(&Exemplar::load(), &vo, &HashMap::new(), &HashMap::new());
386
+
387
+ assert!(generated.contains("pub on: bool,"));
388
+ assert!(generated.contains("pub off: bool,"));
389
+ assert!(generated.contains("Value::Bool(self.on)"));
390
+ assert!(!generated.contains("TrueClass,"));
391
+ assert!(!generated.contains("FalseClass,"));
392
+ }
393
+
394
+ #[test]
395
+ fn boolean_attributes_round_trip_through_json_as_bool() {
396
+ let vo = flag_value_object();
397
+ let attributes = vo.get("attributes").map(Json::each).unwrap_or(&[]);
398
+ let by_name = HashMap::new();
399
+ let exemplar = Exemplar::load();
400
+ let from_json = crate::json_codec::emit_from_json_flat(&exemplar, "Entry", attributes, &by_name, None, None, false, false, None);
401
+ let to_json = crate::json_codec::emit_to_json_flat(&exemplar, "Entry", attributes, &by_name, false, &[], None);
402
+
403
+ assert!(from_json.contains("x.as_bool()"));
404
+ assert!(!from_json.contains("TrueClass::from_json"));
405
+ assert!(to_json.contains("crate::kernel::Json::Bool(self.on)"));
406
+ }
407
+ }
@@ -1 +1 @@
1
- 3.1.0
1
+ 3.1.2
@@ -0,0 +1,39 @@
1
+ use super::*;
2
+
3
+ #[test]
4
+ fn cents_render_as_dollars_with_two_places() {
5
+ assert_eq!(dollars(10800), "$108.00");
6
+ assert_eq!(dollars(14805), "$148.05");
7
+ assert_eq!(dollars(5), "$0.05");
8
+ }
9
+
10
+ #[test]
11
+ fn the_built_in_receipt_names_the_event_the_first_name_and_the_amount() {
12
+ let body = built_in_body(Some("Ada"), "Yogadelics", Some(10800));
13
+ assert!(body.starts_with("Hi Ada,"));
14
+ assert!(body.contains("You're registered for Yogadelics."));
15
+ assert!(body.contains("$108.00"));
16
+ assert_eq!(receipt_subject("Yogadelics"), "You're registered for Yogadelics");
17
+ }
18
+
19
+ #[test]
20
+ fn a_built_in_receipt_without_a_name_or_amount_still_reads_cleanly() {
21
+ let body = built_in_body(None, "Yogadelics", None);
22
+ assert!(body.starts_with("Hi,"));
23
+ assert!(!body.contains("payment of"));
24
+ }
25
+
26
+ #[test]
27
+ fn the_sites_finished_email_is_read() {
28
+ let answer = serde_json::json!({"subject": "You're registered for Yogadelics", "body": "Hi Ada,\n\nWhere: Somewhere\n"});
29
+ assert_eq!(
30
+ site_email_from_json(&answer),
31
+ Some(SiteEmail { subject: "You're registered for Yogadelics".into(), body: "Hi Ada,\n\nWhere: Somewhere\n".into() })
32
+ );
33
+ }
34
+
35
+ #[test]
36
+ fn an_incomplete_answer_from_the_site_falls_back_to_the_built_in_wording() {
37
+ assert_eq!(site_email_from_json(&serde_json::json!({"error": "no such session"})), None);
38
+ assert_eq!(site_email_from_json(&serde_json::json!({"subject": "Hi", "body": " "})), None);
39
+ }
@@ -0,0 +1,122 @@
1
+ // The email a registrant gets once their payment has gone through: what they
2
+ // booked and what they paid. Best-effort, like every other mail the host
3
+ // sends: the payment stands whatever delivery does.
4
+
5
+ use super::instances_for;
6
+ use crate::dispatch;
7
+ use crate::ir::PaymentsProvider;
8
+ use crate::journal::LineageConfig;
9
+ use crate::resend::{Email, Mailer};
10
+ use serde_json::{json, Value};
11
+ use std::path::Path;
12
+ use tokio::sync::Mutex;
13
+ use tokio_postgres::Client;
14
+
15
+ #[cfg(test)]
16
+ mod tests;
17
+
18
+ fn dollars(cents: i64) -> String {
19
+ format!("${}.{:02}", cents / 100, cents % 100)
20
+ }
21
+
22
+ fn receipt_subject(event_name: &str) -> String {
23
+ format!("You're registered for {event_name}")
24
+ }
25
+
26
+ /// The subject and body the site built from the wording an editor keeps in
27
+ /// the CMS (when, where and how-to-prepare are filled in there: the venue lives
28
+ /// only in the CMS).
29
+ #[derive(Debug, PartialEq)]
30
+ struct SiteEmail {
31
+ subject: String,
32
+ body: String,
33
+ }
34
+
35
+ fn site_email_from_json(body: &Value) -> Option<SiteEmail> {
36
+ let text = |key: &str| body.get(key).and_then(Value::as_str).filter(|s| !s.trim().is_empty()).map(String::from);
37
+ Some(SiteEmail { subject: text("subject")?, body: text("body")? })
38
+ }
39
+
40
+ /// Asks the site for the finished email (signed, short-lived, bound to this
41
+ /// one event). Best effort: any failure means the host's own built-in wording
42
+ /// goes out instead, so a receipt is always sent.
43
+ async fn site_email(event_slug: &str, first_name: Option<&str>, amount_cents: Option<i64>) -> Option<SiteEmail> {
44
+ let secret = std::env::var("SESSION_SECRET").ok().filter(|s| !s.is_empty())?;
45
+ let token = crate::auth::purpose_token(&secret, "confirmation-email", json!({ "event": event_slug }), 300);
46
+ let mut url = reqwest::Url::parse(&format!("{}/api/confirmation-email", super::newsletter_send::site_url())).ok()?;
47
+ {
48
+ let mut query = url.query_pairs_mut();
49
+ query.append_pair("event", event_slug);
50
+ if let Some(name) = first_name {
51
+ query.append_pair("first_name", name);
52
+ }
53
+ if let Some(cents) = amount_cents {
54
+ query.append_pair("amount_cents", &cents.to_string());
55
+ }
56
+ }
57
+ let response = reqwest::Client::new().get(url).header("x-internal-auth", token).timeout(std::time::Duration::from_secs(8)).send().await.ok()?;
58
+ if !response.status().is_success() {
59
+ return None;
60
+ }
61
+ site_email_from_json(&response.json::<Value>().await.ok()?)
62
+ }
63
+
64
+ /// The host's own wording, used when the site cannot be asked.
65
+ fn built_in_body(first_name: Option<&str>, event_name: &str, amount_cents: Option<i64>) -> String {
66
+ let greeting = first_name.map(|name| format!("Hi {name},")).unwrap_or_else(|| "Hi,".to_string());
67
+ let paid = amount_cents.map(|cents| format!(" We received your payment of {}.", dollars(cents))).unwrap_or_default();
68
+ format!("{greeting}\n\nYou're registered for {event_name}.{paid}\n\nYour seat is held. If anything changes, just reply to this email.\n")
69
+ }
70
+
71
+ /// Mails the receipt for `reference` if its Payment has succeeded. A missing
72
+ /// mailer, registrant address or event is skipped quietly.
73
+ pub(super) async fn send_receipt(reference: &str, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, payments: &PaymentsProvider) {
74
+ let mailer = match Mailer::from_env() {
75
+ Ok(Some(mailer)) => mailer,
76
+ Ok(None) => return,
77
+ Err(e) => {
78
+ eprintln!("registration receipt: {e}");
79
+ return;
80
+ }
81
+ };
82
+ let Ok(read) = dispatch::read(client, wasm_path).await else {
83
+ return;
84
+ };
85
+ let binding = crate::ir::registrations_binding(&config.domain);
86
+ let registrations = instances_for(&read, &binding.registration_prefix());
87
+ let Some((_, registration)) = registrations.iter().find(|(id, _)| id == reference) else {
88
+ return;
89
+ };
90
+ let payment_instances = instances_for(&read, &payments.instance_prefix());
91
+ let payment = payment_instances.iter().find(|(id, _)| id == reference).map(|(_, p)| p);
92
+ if payment.and_then(|p| p.get("status")).and_then(|s| s.as_str()) != Some("succeeded") {
93
+ return;
94
+ }
95
+ let attendee = registration.get("attendee").cloned().unwrap_or_else(|| serde_json::json!({}));
96
+ let Some(to) = attendee.get("email").and_then(|v| v.as_str()).filter(|s| !s.is_empty()) else {
97
+ return;
98
+ };
99
+ let events = instances_for(&read, &binding.event_prefix());
100
+ let event_name = registration
101
+ .get("event_slug")
102
+ .and_then(|v| v.as_str())
103
+ .and_then(|slug| events.iter().find(|(id, _)| id == slug))
104
+ .and_then(|(_, e)| e.get("name"))
105
+ .and_then(|n| n.get("value"))
106
+ .and_then(|v| v.as_str())
107
+ .unwrap_or("your event");
108
+ let amount = payment.and_then(|p| p.get("amount")).and_then(|a| a.get("cents")).and_then(Value::as_i64);
109
+ let first_name = attendee.get("first_name").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
110
+ let from_site = match registration.get("event_slug").and_then(|v| v.as_str()) {
111
+ Some(slug) => site_email(slug, first_name, amount).await,
112
+ None => None,
113
+ };
114
+ let (subject, body) = match from_site {
115
+ Some(email) => (email.subject, email.body),
116
+ None => (receipt_subject(event_name), built_in_body(first_name, event_name, amount)),
117
+ };
118
+ let delivery = mailer.deliver(&Email { to, subject: &subject, body: &body, unsubscribe_url: None }).await;
119
+ if !delivery.ok {
120
+ eprintln!("registration receipt: not delivered: {}", delivery.reason.unwrap_or_default());
121
+ }
122
+ }
@@ -367,6 +367,12 @@ pub(crate) async fn registration_complete_route(
367
367
  }
368
368
  }
369
369
 
370
+ // Only a payment that settled just now earns a receipt; a repeat click was
371
+ // refused above as already settled.
372
+ if outcome == "succeeded" && outcome_result.accepted {
373
+ super::registration_receipt::send_receipt(registration_id, client, wasm_path, config, payments).await;
374
+ }
375
+
370
376
  let read = match dispatch::read(client, wasm_path).await {
371
377
  Ok(r) => r,
372
378
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
@@ -675,8 +681,14 @@ pub(crate) async fn webhook_route(
675
681
  // A refusal here (e.g. a redelivered webhook — Stripe's delivery
676
682
  // is at-least-once) is a benign no-op: the payment already holds
677
683
  // the right status, and 200 tells Stripe's retry logic to stop.
678
- if let Err(e) = dispatch::handle_routed(client, wasm_path, verb, json!(reference), facts, None, config, invoker).await {
679
- return respond(500, "text/plain", &format!("{e:#}"));
684
+ match dispatch::handle_routed(client, wasm_path, verb, json!(reference), facts, None, config, invoker).await {
685
+ Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
686
+ // Only the delivery that settles the payment sends the receipt;
687
+ // a redelivery is refused and stays quiet.
688
+ Ok(settled) if settled.accepted && event_type == "checkout.session.completed" => {
689
+ super::registration_receipt::send_receipt(&reference, client, wasm_path, config, payments).await;
690
+ }
691
+ Ok(_) => {}
680
692
  }
681
693
  }
682
694
  }
data/rust/host/src/web.rs CHANGED
@@ -18,6 +18,7 @@ use tokio_postgres::Client;
18
18
 
19
19
  mod newsletter;
20
20
  mod newsletter_send;
21
+ mod registration_receipt;
21
22
  mod registrations;
22
23
 
23
24
  use registrations::payments_routes;
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hecks
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-03 00:00:00.000000000 Z
11
+ date: 2026-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -216,10 +216,14 @@ files:
216
216
  - lib/hecks/cli/doc_coverage.rb
217
217
  - lib/hecks/cli/document.rb
218
218
  - lib/hecks/cli/domain_stub.rb
219
+ - lib/hecks/cli/domain_writer.rb
219
220
  - lib/hecks/cli/expression_projection.rb
220
221
  - lib/hecks/cli/follow.rb
221
222
  - lib/hecks/cli/history.rb
223
+ - lib/hecks/cli/interview_agent.rb
222
224
  - lib/hecks/cli/interview_draft.rb
225
+ - lib/hecks/cli/interview_run.rb
226
+ - lib/hecks/cli/interview_session.rb
223
227
  - lib/hecks/cli/ir.rb
224
228
  - lib/hecks/cli/mcp.rb
225
229
  - lib/hecks/cli/model_check.rb
@@ -264,6 +268,7 @@ files:
264
268
  - lib/hecks/doors/ruby_door.rb
265
269
  - lib/hecks/doors/ruby_door/aggregate_door.rb
266
270
  - lib/hecks/doors/ruby_door/chapter.rb
271
+ - lib/hecks/doors/usage_cache.rb
267
272
  - lib/hecks/embryonaut_bluebook.rb
268
273
  - lib/hecks/embryonaut_bluebook/lock.rb
269
274
  - lib/hecks/embryonaut_bluebook/shape.rb
@@ -369,6 +374,8 @@ files:
369
374
  - lib/hecks/hecks/adapters/console_capture.rb
370
375
  - lib/hecks/hecks/adapters/deploy_toolchain.adapter
371
376
  - lib/hecks/hecks/adapters/deploy_toolchain.rb
377
+ - lib/hecks/hecks/adapters/finding_github.adapter
378
+ - lib/hecks/hecks/adapters/finding_github.rb
372
379
  - lib/hecks/hecks/adapters/git.adapter
373
380
  - lib/hecks/hecks/adapters/git.rb
374
381
  - lib/hecks/hecks/adapters/host_http.adapter
@@ -533,7 +540,10 @@ files:
533
540
  - lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl
534
541
  - lib/hecks/projections/deploy/box/templates/fetch-secrets.sh
535
542
  - lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl
543
+ - lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl
536
544
  - lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl
545
+ - lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl
546
+ - lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl
537
547
  - lib/hecks/projections/deploy/fargate.rb
538
548
  - lib/hecks/projections/deploy/fargate/assembly.rb
539
549
  - lib/hecks/projections/deploy/fargate/cdn.rb
@@ -763,6 +773,9 @@ files:
763
773
  - lib/hecks/tenancy/bluebook/tenancy.hecksagon
764
774
  - lib/hecks/three_zero.rb
765
775
  - lib/hecks/three_zero/forms.yml
776
+ - lib/hecks/tickets/bluebook/tickets.bluebook
777
+ - lib/hecks/tickets/bluebook/tickets.hecksagon
778
+ - lib/hecks/tickets/bluebook/tickets.ports.hecksagon
766
779
  - lib/hecks/tools.rb
767
780
  - lib/hecks/tools/argument_gate_matrix.rb
768
781
  - lib/hecks/tools/ci_gate_decision.rb
@@ -897,6 +910,8 @@ files:
897
910
  - rust/host/src/web/newsletter/template.rs
898
911
  - rust/host/src/web/newsletter/tests.rs
899
912
  - rust/host/src/web/newsletter_send.rs
913
+ - rust/host/src/web/registration_receipt.rs
914
+ - rust/host/src/web/registration_receipt/tests.rs
900
915
  - rust/host/src/web/registrations.rs
901
916
  - rust/lsp/.gitignore
902
917
  - rust/lsp/Cargo.lock