hecks 3.2.1 → 3.4.0

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +21 -23
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/interview_draft.rb +88 -33
  10. data/lib/hecks/cli/interview_session.rb +77 -26
  11. data/lib/hecks/cli/interview_shape.rb +198 -0
  12. data/lib/hecks/corpus.rb +3 -5
  13. data/lib/hecks/doors/cli_runner.rb +14 -1
  14. data/lib/hecks/doors/launcher_options.rb +13 -3
  15. data/lib/hecks/doors/mcp_door_scope.rb +2 -1
  16. data/lib/hecks/doors.rb +0 -17
  17. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  18. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  19. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  20. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  21. data/lib/hecks/fuzzing/isolated_boot.rb +1 -4
  22. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  23. data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
  24. data/lib/hecks/gate/stages.yml +113 -8
  25. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  26. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  27. data/lib/hecks/hecks/adapters/git.rb +59 -0
  28. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  29. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  30. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  31. data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
  32. data/lib/hecks/hecks/codebase.behaviors +6 -0
  33. data/lib/hecks/hecks/codebase.bluebook +21 -0
  34. data/lib/hecks/hecks/custodian.behaviors +6 -0
  35. data/lib/hecks/hecks/custodian.bluebook +217 -0
  36. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  37. data/lib/hecks/hecks/hecks.world +8 -3
  38. data/lib/hecks/hecks/site.world +5 -2
  39. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  40. data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
  41. data/lib/hecks/language/hecksagon/hecksagon.bluebook +0 -5
  42. data/lib/hecks/language/oidc.json +5 -0
  43. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  44. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  45. data/lib/hecks/projections/deploy/box/settings.rb +17 -4
  46. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  47. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  48. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  49. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  50. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  51. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  52. data/lib/hecks/projections/deploy/box.rb +15 -2
  53. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  54. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  55. data/lib/hecks/projections/model/deviations.rb +1 -1
  56. data/lib/hecks/projections/site/live_edge.rb +162 -0
  57. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  58. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  59. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  60. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  61. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  62. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  63. data/lib/hecks/runtime/interpreting.rb +3 -22
  64. data/lib/hecks/runtime/loader.rb +6 -7
  65. data/lib/hecks/runtime/needs.rb +48 -0
  66. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  67. data/lib/hecks/runtime/registry/verification.rb +3 -3
  68. data/lib/hecks/runtime.rb +6 -11
  69. data/lib/hecks/rust_build/host.rb +147 -0
  70. data/lib/hecks/rust_build/kernel_input.rb +40 -18
  71. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  72. data/lib/hecks/rust_build/project_rust.rb +3 -10
  73. data/lib/hecks/rust_build.rb +1 -0
  74. data/lib/hecks/site/bluebook/site.bluebook +53 -1
  75. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  76. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  77. data/lib/hecks/tools/gate.rb +1 -1
  78. data/lib/hecks/tools/rust_comment_style.rb +1 -3
  79. data/lib/hecks/tools/tools_doc.rb +204 -0
  80. data/lib/hecks/tools.rb +1 -0
  81. data/lib/hecks/version.rb +1 -1
  82. data/lib/hecks/vocabulary.rb +151 -0
  83. data/lib/hecks.rb +6 -12
  84. data/qa/settings.yml +1 -2
  85. data/rust/build/src/lineage_pass.rs +30 -15
  86. data/rust/build/src/main.rs +4 -2
  87. data/rust/build/src/pipeline.rs +8 -19
  88. data/rust/codegen/src/bridging.rs +7 -7
  89. data/rust/codegen/src/json.rs +214 -23
  90. data/rust/codegen/src/literal.rs +6 -3
  91. data/rust/codegen/src/main.rs +20 -9
  92. data/rust/codegen/src/naming.rs +4 -2
  93. data/rust/codegen/src/optional_pass.rs +226 -0
  94. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  95. data/rust/host/HECKS_RELEASE +1 -1
  96. data/rust/host/src/api.rs +1 -1
  97. data/rust/host/src/dispatch.rs +5 -0
  98. data/rust/host/src/needs.rs +99 -2
  99. data/rust/host/src/resend.rs +4 -4
  100. data/rust/host/src/ui_schema.rs +1 -1
  101. data/rust/parser/src/emit.rs +12 -0
  102. data/rust/parser/src/ir.rs +2 -0
  103. data/rust/parser/src/keywords.rs +2 -4
  104. data/rust/parser/src/lex.rs +1 -1
  105. data/rust/parser/src/main.rs +1 -0
  106. data/rust/parser/src/parse/command.rs +2 -44
  107. data/rust/parser/src/parse/hecksagon.rs +15 -24
  108. data/rust/parser/src/parse/mod.rs +1 -0
  109. data/rust/parser/src/parse/needs.rs +60 -0
  110. data/rust/parser/src/parse/query.rs +3 -0
  111. data/rust/src/kernel/cli.rs +16 -6
  112. data/rust/src/kernel/needs.rs +104 -9
  113. metadata +22 -9
  114. data/lib/hecks/facade.rb +0 -31
  115. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  116. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  117. data/lib/hecks/three_zero/forms.yml +0 -127
  118. data/lib/hecks/three_zero.rb +0 -12
  119. data/rust/build/src/json.rs +0 -421
  120. data/rust/build/src/optional_pass.rs +0 -150
@@ -164,7 +164,7 @@ pub fn parse_body(
164
164
  owner_entities,
165
165
  );
166
166
  refuse_duplicate_targets(file, *pos, &command)?;
167
- refuse_undeclared_needs(file, *pos, &command)?;
167
+ super::needs::refuse_undeclared(file, *pos, &command.name, &command.needs, &command.attributes)?;
168
168
  return Ok((command, pending));
169
169
  };
170
170
  let line = gated.line.number;
@@ -206,27 +206,7 @@ pub fn parse_body(
206
206
  canonical: canonical::apply(&raw),
207
207
  });
208
208
  }
209
- "needs" => {
210
- let fact = super::positional_symbol(file, line, "needs", &gated.args, 1)?;
211
- if !NEEDABLE_FACTS.contains(&fact.as_str()) {
212
- return Err(Diagnostic::new(
213
- file,
214
- line,
215
- format!(
216
- "{} needs :{fact}, which the runtime cannot supply — it supplies :now",
217
- command.name
218
- ),
219
- ));
220
- }
221
- if command.needs.contains(&fact) {
222
- return Err(Diagnostic::new(
223
- file,
224
- line,
225
- format!("{} declares needs :{fact} twice", command.name),
226
- ));
227
- }
228
- command.needs.push(fact);
229
- }
209
+ "needs" => super::needs::declare(file, line, &command.name, &gated.args, &mut command.needs)?,
230
210
  "provenance" => {
231
211
  let raw = super::named_raw(&gated.args, "from")
232
212
  .ok_or_else(|| Diagnostic::new(file, line, "'provenance' requires a from:"))?;
@@ -319,28 +299,6 @@ fn state_ref(raw: &str) -> Option<String> {
319
299
  Some(inner.to_string())
320
300
  }
321
301
 
322
- /// The outside facts a command may declare it needs; mirrors `CommandBuilder::NEEDABLE_FACTS`.
323
- const NEEDABLE_FACTS: &[&str] = &["now"];
324
-
325
- /// A need names the attribute the runtime fills, so the command must declare it.
326
- fn refuse_undeclared_needs(file: &str, line: usize, command: &ir::Command) -> ParseResult<()> {
327
- match command
328
- .needs
329
- .iter()
330
- .find(|fact| !command.attributes.iter().any(|a| &a.name == *fact))
331
- {
332
- Some(fact) => Err(Diagnostic::new(
333
- file,
334
- line,
335
- format!(
336
- "{} needs :{fact} but declares no attribute :{fact} for the runtime to fill — add `attribute :{fact}, <type>`",
337
- command.name
338
- ),
339
- )),
340
- None => Ok(()),
341
- }
342
- }
343
-
344
302
  /// Refuses a field written twice: a command's effects are one update set (C4.2).
345
303
  /// `delegates_to`/`corrects` name no field and are not counted.
346
304
  fn refuse_duplicate_targets(file: &str, line: usize, command: &ir::Command) -> ParseResult<()> {
@@ -14,9 +14,8 @@ pub fn not_implemented(file: &str, line: usize, word: &str) -> Diagnostic {
14
14
  /// Applies a `.hecksagon` body onto an already-built `ir::Bluebook`, attaching
15
15
  /// aggregate-scoped ports to the aggregates the sibling `.bluebook` registered.
16
16
  ///
17
- /// `gem_chapter_names` collects each `attaches "Name"` (and the deprecated `uses_framework`) and
18
- /// `vendored_bluebook_names` each `attaches "name", from: :vendor` (and the deprecated
19
- /// `uses_embryonaut_bluebook`), in file order, for `hecks-parse resolve`; `ir.json` itself
17
+ /// `gem_chapter_names` collects each `attaches "Name"` and
18
+ /// `vendored_bluebook_names` each `attaches "name", from: :vendor`, in file order, for `hecks-parse resolve`; `ir.json` itself
20
19
  /// carries neither.
21
20
  ///
22
21
  /// `require_matching_aggregate` is `false` for resolve, which never sees a `.bluebook`, so an
@@ -65,8 +64,20 @@ pub fn apply(
65
64
  if let LineShape::Call(call) = lex::classify(file, &line)? {
66
65
  if !matches!(
67
66
  call.word.as_str(),
68
- "port" | "subscribe" | "uses_framework" | "uses_embryonaut_bluebook" | "attaches" | "translates" | "bounded" | "end"
67
+ "port" | "subscribe" | "attaches" | "translates" | "bounded" | "end"
69
68
  ) {
69
+ let instead = match call.word.as_str() {
70
+ "uses_framework" => Some("attaches \"Name\""),
71
+ "uses_embryonaut_bluebook" => Some("attaches \"name\", from: :vendor"),
72
+ _ => None,
73
+ };
74
+ if let Some(instead) = instead {
75
+ return Err(Diagnostic::new(
76
+ file,
77
+ line.number,
78
+ format!("`{}` was removed in 3.4.0; use `{instead}`", call.word),
79
+ ));
80
+ }
70
81
  *pos += 1;
71
82
  if matches!(call.opener, Opener::DoBlock { .. }) {
72
83
  skip_dropped_body(file, lines, pos)?;
@@ -98,26 +109,6 @@ pub fn apply(
98
109
  let built = policy::parse_body(file, lines, pos, &name)?;
99
110
  bluebook.policies.push(built);
100
111
  }
101
- // Deprecated spelling of `attaches "Name"`; gated, then collected.
102
- "uses_framework" => {
103
- gem_chapter_names.push(super::positional_text(
104
- file,
105
- gated.line.number,
106
- "uses_framework",
107
- &gated.args,
108
- 1,
109
- )?);
110
- }
111
- // Deprecated spelling of `attaches "name", from: :vendor`; collected the same way.
112
- "uses_embryonaut_bluebook" => {
113
- vendored_bluebook_names.push(super::positional_text(
114
- file,
115
- gated.line.number,
116
- "uses_embryonaut_bluebook",
117
- &gated.args,
118
- 1,
119
- )?);
120
- }
121
112
  "subscribe" => {}
122
113
  // One word for a chapter the gem carries and for a vendored package; a binding
123
114
  // fact collected for `resolve`, absent from `ir.json`. Only `from: :vendor`
@@ -9,6 +9,7 @@ pub mod entity;
9
9
  pub mod file;
10
10
  pub mod hecksagon;
11
11
  pub mod lifecycle;
12
+ pub mod needs;
12
13
  pub mod policy;
13
14
  pub mod process_manager;
14
15
  pub mod query;
@@ -0,0 +1,60 @@
1
+ //! The `needs` word, shared by the two declarations that may use it: a command and a query
2
+ //! (ADR 0081). Mirrors `Hecks::Bluebook::DSL::NeedWord`.
3
+
4
+ use crate::diag::{Diagnostic, ParseResult};
5
+ use crate::ir;
6
+ use crate::parse::ArgumentGateResult;
7
+
8
+ /// The outside facts a declaration may need, each the name of the attribute the runtime fills;
9
+ /// mirrors `NeedWord::NEEDABLE_FACTS`. `now` is the clock port's reading in epoch seconds and
10
+ /// `today` the day it falls in, whole days since the epoch in UTC.
11
+ const NEEDABLE_FACTS: &[&str] = &["now", "today"];
12
+
13
+ /// Reads one `needs :fact` line into `needs`.
14
+ ///
15
+ /// Refuses a fact the runtime cannot supply and a fact declared twice, as the Ruby builder does.
16
+ pub fn declare(
17
+ file: &str,
18
+ line: usize,
19
+ owner: &str,
20
+ args: &ArgumentGateResult,
21
+ needs: &mut Vec<String>,
22
+ ) -> ParseResult<()> {
23
+ let fact = super::positional_symbol(file, line, "needs", args, 1)?;
24
+ if !NEEDABLE_FACTS.contains(&fact.as_str()) {
25
+ let supplied: Vec<String> = NEEDABLE_FACTS.iter().map(|known| format!(":{known}")).collect();
26
+ return Err(Diagnostic::new(
27
+ file,
28
+ line,
29
+ format!(
30
+ "{owner} needs :{fact}, which the runtime cannot supply — it supplies {}",
31
+ supplied.join(", ")
32
+ ),
33
+ ));
34
+ }
35
+ if needs.contains(&fact) {
36
+ return Err(Diagnostic::new(file, line, format!("{owner} declares needs :{fact} twice")));
37
+ }
38
+ needs.push(fact);
39
+ Ok(())
40
+ }
41
+
42
+ /// A need names the attribute the runtime fills, so the declaration must have one.
43
+ pub fn refuse_undeclared(
44
+ file: &str,
45
+ line: usize,
46
+ owner: &str,
47
+ needs: &[String],
48
+ attributes: &[ir::Attribute],
49
+ ) -> ParseResult<()> {
50
+ match needs.iter().find(|fact| !attributes.iter().any(|a| &a.name == *fact)) {
51
+ Some(fact) => Err(Diagnostic::new(
52
+ file,
53
+ line,
54
+ format!(
55
+ "{owner} needs :{fact} but declares no attribute :{fact} for the runtime to fill — add `attribute :{fact}, <type>`"
56
+ ),
57
+ )),
58
+ None => Ok(()),
59
+ }
60
+ }
@@ -25,6 +25,8 @@ pub fn parse_body(
25
25
 
26
26
  loop {
27
27
  let Some(gated) = super::next_line(file, lines, pos, "Query")? else {
28
+ let end = lines.get(pos.saturating_sub(1)).map_or(0, |l| l.number);
29
+ super::needs::refuse_undeclared(file, end, name, &query.needs, &query.attributes)?;
28
30
  return Ok(query);
29
31
  };
30
32
  let line = gated.line.number;
@@ -83,6 +85,7 @@ pub fn parse_body(
83
85
  type_name
84
86
  });
85
87
  }
88
+ "needs" => super::needs::declare(file, line, name, &gated.args, &mut query.needs)?,
86
89
  word if OPTION_WORDS.contains(&word) => {
87
90
  query_options::apply(file, line, word, &gated.args, &mut query.options)?
88
91
  }
@@ -18,6 +18,8 @@ pub fn run(input: &str) -> String {
18
18
 
19
19
  // Optional `"needs"`: which commands need which outside facts (ADR 0081), passed by the host.
20
20
  super::needs::install(parsed.get("needs"));
21
+ // Optional `"query_needs"`: which queries need which outside facts, filled the same way.
22
+ super::needs::install_queries(parsed.get("query_needs"));
21
23
  // Optional `"defaults"`: each command's declared argument defaults, filled the same way.
22
24
  super::needs::install_defaults(parsed.get("defaults"));
23
25
 
@@ -86,6 +88,14 @@ pub fn run(input: &str) -> String {
86
88
  // A query step's `args` sit at the sibling `"args"` key, as for a command step.
87
89
  let empty_args = Json::Object(vec![]);
88
90
  let args = step.get("args").unwrap_or(&empty_args);
91
+ // What the caller offered is what the log echoes; a fact the query `needs` is answered
92
+ // for the run alone (ADR 0081), so the log holds no value that depends on the clock.
93
+ let offered = args;
94
+ let filled = match step.get("query") {
95
+ Some(Json::Str(question)) => super::needs::enrich_query(question, args, step.get("occurred_at").and_then(Json::as_str)),
96
+ _ => None,
97
+ };
98
+ let args = filled.as_ref().unwrap_or(args);
89
99
 
90
100
  // A `"query"` step carries no verb and takes one of three shapes:
91
101
  // - `"Domain::Aggregate.Query"`: a declared aggregate query, looked up in `QUERIES`.
@@ -106,7 +116,7 @@ pub fn run(input: &str) -> String {
106
116
  let rows = Json::Array(rows);
107
117
  query_results.push(Json::obj(vec![
108
118
  ("query", Json::Str(question.clone())),
109
- ("args", args.clone()),
119
+ ("args", offered.clone()),
110
120
  ("rows", rows.clone()),
111
121
  ("reference_rows", rows),
112
122
  ]));
@@ -115,7 +125,7 @@ pub fn run(input: &str) -> String {
115
125
  let message = refusal.to_string();
116
126
  query_results.push(Json::obj(vec![
117
127
  ("query", Json::Str(question.clone())),
118
- ("args", args.clone()),
128
+ ("args", offered.clone()),
119
129
  ("rows", Json::Null),
120
130
  ("error", Json::Str(message.clone())),
121
131
  ("reference_rows", Json::Null),
@@ -131,7 +141,7 @@ pub fn run(input: &str) -> String {
131
141
  let rows = Json::Array(entries.into_iter().map(|(id, record)| repository::row_json(id, record)).collect());
132
142
  query_results.push(Json::obj(vec![
133
143
  ("query", Json::Str(question.clone())),
134
- ("args", args.clone()),
144
+ ("args", offered.clone()),
135
145
  ("rows", rows.clone()),
136
146
  // Same as `rows`: one compiled path (named_query.rs).
137
147
  ("reference_rows", rows),
@@ -143,7 +153,7 @@ pub fn run(input: &str) -> String {
143
153
  let message = refusal.to_string();
144
154
  query_results.push(Json::obj(vec![
145
155
  ("query", Json::Str(question.clone())),
146
- ("args", args.clone()),
156
+ ("args", offered.clone()),
147
157
  ("rows", Json::Null),
148
158
  ("error", Json::Str(message.clone())),
149
159
  ("reference_rows", Json::Null),
@@ -170,7 +180,7 @@ pub fn run(input: &str) -> String {
170
180
  Ok(row) => {
171
181
  query_results.push(Json::obj(vec![
172
182
  ("query", Json::Str(question.clone())),
173
- ("args", args.clone()),
183
+ ("args", offered.clone()),
174
184
  ("rows", Json::Array(vec![row])),
175
185
  // No `reference_rows` key: Ruby sets it only for questions with a
176
186
  // reference twin, and a read model has none.
@@ -181,7 +191,7 @@ pub fn run(input: &str) -> String {
181
191
  let message = refusal.to_string();
182
192
  query_results.push(Json::obj(vec![
183
193
  ("query", Json::Str(question.clone())),
184
- ("args", args.clone()),
194
+ ("args", offered.clone()),
185
195
  ("rows", Json::Null),
186
196
  ("error", Json::Str(message)),
187
197
  ]));
@@ -26,15 +26,16 @@ pub struct Need {
26
26
  thread_local! {
27
27
  static TABLE: RefCell<HashMap<String, Vec<Need>>> = RefCell::new(HashMap::new());
28
28
  static DEFAULTS: RefCell<HashMap<String, Vec<(String, Json)>>> = RefCell::new(HashMap::new());
29
+ static QUERY_TABLE: RefCell<HashMap<String, Vec<Need>>> = RefCell::new(HashMap::new());
29
30
  static FIXED_CLOCK: Cell<Option<i64>> = const { Cell::new(None) };
30
31
  }
31
32
 
32
- /// Installs the table the host passed (an absent or malformed `"needs"` installs an empty one).
33
- pub fn install(table: Option<&Json>) {
34
- let parsed = match table {
33
+ /// Reads a `{ name: [{fact, type}] }` table (an absent or malformed one reads as empty).
34
+ fn parse_table(table: Option<&Json>) -> HashMap<String, Vec<Need>> {
35
+ match table {
35
36
  Some(Json::Object(entries)) => entries
36
37
  .iter()
37
- .map(|(verb, needs)| {
38
+ .map(|(name, needs)| {
38
39
  let needs = needs
39
40
  .as_array()
40
41
  .unwrap_or(&[])
@@ -45,12 +46,23 @@ pub fn install(table: Option<&Json>) {
45
46
  Some(Need { fact, attribute_type })
46
47
  })
47
48
  .collect();
48
- (verb.clone(), needs)
49
+ (name.clone(), needs)
49
50
  })
50
51
  .collect(),
51
52
  _ => HashMap::new(),
52
- };
53
- TABLE.with(|t| *t.borrow_mut() = parsed);
53
+ }
54
+ }
55
+
56
+ /// Installs the table the host passed, command verb to its needs (an absent or malformed
57
+ /// `"needs"` installs an empty one).
58
+ pub fn install(table: Option<&Json>) {
59
+ TABLE.with(|t| *t.borrow_mut() = parse_table(table));
60
+ }
61
+
62
+ /// Installs the table of queries that need a fact, `"Domain::Aggregate.Query"` to its needs. Kept
63
+ /// apart from the commands': a command and a query may share a qualified name.
64
+ pub fn install_queries(table: Option<&Json>) {
65
+ QUERY_TABLE.with(|t| *t.borrow_mut() = parse_table(table));
54
66
  }
55
67
 
56
68
  /// Installs the declared defaults the host passed, `{ verb: { attribute: value } }` (an absent or
@@ -106,9 +118,11 @@ fn parse_iso8601_utc(text: &str) -> Option<i64> {
106
118
 
107
119
  fn answer(need: &Need, occurred_at: Option<&str>) -> Option<Json> {
108
120
  match need.fact.as_str() {
109
- "now" => {
121
+ // `now` is epoch seconds; `today` the day those fall in, whole days since the epoch (UTC).
122
+ "now" | "today" => {
110
123
  let secs = now_secs(occurred_at);
111
- Some(if need.attribute_type == "Integer" { Json::int(secs) } else { Json::obj(vec![("value", Json::int(secs))]) })
124
+ let answer = if need.fact == "today" { secs.div_euclid(86_400) } else { secs };
125
+ Some(if need.attribute_type == "Integer" { Json::int(answer) } else { Json::obj(vec![("value", Json::int(answer))]) })
112
126
  }
113
127
  _ => None,
114
128
  }
@@ -151,6 +165,27 @@ pub fn enrich(verb: &str, args: &Json, occurred_at: Option<&str>) -> Option<Json
151
165
  Some(Json::Object(envelope.iter().map(|(k, v)| if k == "with" { (k.clone(), Json::Object(filled.clone())) } else { (k.clone(), v.clone()) }).collect()))
152
166
  }
153
167
 
168
+ /// `args` with each fact the query `question` needs and the caller left out answered, or `None`
169
+ /// when nothing needed filling (the query needs nothing, or every fact is supplied). A query's
170
+ /// arguments are the flat object of its step.
171
+ pub fn enrich_query(question: &str, args: &Json, occurred_at: Option<&str>) -> Option<Json> {
172
+ let needs = QUERY_TABLE.with(|t| t.borrow().get(question).cloned())?;
173
+ let Json::Object(held) = args else { return None };
174
+ let mut filled = held.clone();
175
+ for need in &needs {
176
+ if held.iter().any(|(k, _)| k == &need.fact) {
177
+ continue;
178
+ }
179
+ if let Some(value) = answer(need, occurred_at) {
180
+ filled.push((need.fact.clone(), value));
181
+ }
182
+ }
183
+ if filled.len() == held.len() {
184
+ return None;
185
+ }
186
+ Some(Json::Object(filled))
187
+ }
188
+
154
189
  #[cfg(test)]
155
190
  mod tests {
156
191
  use super::*;
@@ -264,4 +299,64 @@ mod tests {
264
299
  install_defaults(None);
265
300
  assert_eq!(enrich("D::A.Start", &with(vec![]), None), None);
266
301
  }
302
+
303
+ fn query_table() -> Json {
304
+ Json::parse(
305
+ r#"{"D::A.Expired": [{"fact": "now", "type": "Integer"}],
306
+ "D::A.Today": [{"fact": "today", "type": "DayNumber"}]}"#,
307
+ )
308
+ .unwrap()
309
+ }
310
+
311
+ #[test]
312
+ fn a_needed_query_fact_the_caller_left_out_is_answered_by_the_clock() {
313
+ install_queries(Some(&query_table()));
314
+ fix_clock(Some(1_700_000_000));
315
+ let filled = enrich_query("D::A.Expired", &Json::obj(vec![]), None).unwrap();
316
+ assert_eq!(filled.get("now"), Some(&Json::int(1_700_000_000)));
317
+ fix_clock(None);
318
+ install_queries(None);
319
+ }
320
+
321
+ #[test]
322
+ fn a_query_value_the_caller_supplied_is_kept() {
323
+ install_queries(Some(&query_table()));
324
+ fix_clock(Some(1));
325
+ assert_eq!(enrich_query("D::A.Expired", &Json::obj(vec![("now", Json::int(7))]), None), None);
326
+ fix_clock(None);
327
+ install_queries(None);
328
+ }
329
+
330
+ #[test]
331
+ fn a_query_that_needs_nothing_is_untouched() {
332
+ install_queries(Some(&query_table()));
333
+ assert_eq!(enrich_query("D::A.Plain", &Json::obj(vec![]), None), None);
334
+ install_queries(None);
335
+ assert_eq!(enrich_query("D::A.Expired", &Json::obj(vec![]), None), None);
336
+ }
337
+
338
+ #[test]
339
+ fn a_command_and_a_query_of_one_name_keep_their_own_needs() {
340
+ install(Some(&table()));
341
+ install_queries(Some(&query_table()));
342
+ fix_clock(Some(5));
343
+ assert_eq!(enrich_query("D::A.Instant", &Json::obj(vec![]), None), None);
344
+ assert!(enrich("D::A.Instant", &with(vec![]), None).is_some());
345
+ fix_clock(None);
346
+ install(None);
347
+ install_queries(None);
348
+ }
349
+
350
+ #[test]
351
+ fn today_is_the_day_the_clock_falls_in_whole_days_since_the_epoch() {
352
+ install_queries(Some(&query_table()));
353
+ fix_clock(Some((19_000 * 86_400) + 86_399));
354
+ let filled = enrich_query("D::A.Today", &Json::obj(vec![]), None).unwrap();
355
+ assert_eq!(filled.get("today"), Some(&Json::obj(vec![("value", Json::int(19_000))])));
356
+ fix_clock(Some(19_001 * 86_400));
357
+ let next = enrich_query("D::A.Today", &Json::obj(vec![]), None).unwrap();
358
+ assert_eq!(next.get("today"), Some(&Json::obj(vec![("value", Json::int(19_001))])));
359
+ fix_clock(None);
360
+ install_queries(None);
361
+ }
267
362
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hecks
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young
@@ -162,6 +162,7 @@ files:
162
162
  - lib/hecks/bluebook/dsl/identity_declaration.rb
163
163
  - lib/hecks/bluebook/dsl/lifecycle_builder.rb
164
164
  - lib/hecks/bluebook/dsl/malformed.rb
165
+ - lib/hecks/bluebook/dsl/need_word.rb
165
166
  - lib/hecks/bluebook/dsl/policy_builder.rb
166
167
  - lib/hecks/bluebook/dsl/port_builder.rb
167
168
  - lib/hecks/bluebook/dsl/port_operation_builder.rb
@@ -232,6 +233,7 @@ files:
232
233
  - lib/hecks/cli/interview_draft.rb
233
234
  - lib/hecks/cli/interview_run.rb
234
235
  - lib/hecks/cli/interview_session.rb
236
+ - lib/hecks/cli/interview_shape.rb
235
237
  - lib/hecks/cli/ir.rb
236
238
  - lib/hecks/cli/mcp.rb
237
239
  - lib/hecks/cli/model_check.rb
@@ -280,11 +282,12 @@ files:
280
282
  - lib/hecks/doors/usage_cache.rb
281
283
  - lib/hecks/embryonaut_bluebook.rb
282
284
  - lib/hecks/embryonaut_bluebook/lock.rb
285
+ - lib/hecks/embryonaut_bluebook/manifest.rb
286
+ - lib/hecks/embryonaut_bluebook/registry.rb
283
287
  - lib/hecks/embryonaut_bluebook/shape.rb
284
288
  - lib/hecks/embryonaut_bluebook/vendor.rb
285
289
  - lib/hecks/embryonaut_bluebook/vendor_cli.rb
286
290
  - lib/hecks/engine_agreement.rb
287
- - lib/hecks/facade.rb
288
291
  - lib/hecks/forms.rb
289
292
  - lib/hecks/forms/app.rb
290
293
  - lib/hecks/forms/banking_presentation.rb
@@ -387,10 +390,12 @@ files:
387
390
  - lib/hecks/hecks/adapters/deploy_toolchain.rb
388
391
  - lib/hecks/hecks/adapters/finding_github.adapter
389
392
  - lib/hecks/hecks/adapters/finding_github.rb
393
+ - lib/hecks/hecks/adapters/first_admin.rb
390
394
  - lib/hecks/hecks/adapters/git.adapter
391
395
  - lib/hecks/hecks/adapters/git.rb
392
396
  - lib/hecks/hecks/adapters/host_http.adapter
393
397
  - lib/hecks/hecks/adapters/host_http.rb
398
+ - lib/hecks/hecks/adapters/in_process_access.rb
394
399
  - lib/hecks/hecks/adapters/in_process_boot.adapter
395
400
  - lib/hecks/hecks/adapters/in_process_boot.rb
396
401
  - lib/hecks/hecks/adapters/in_process_operations.rb
@@ -546,14 +551,22 @@ files:
546
551
  - lib/hecks/projections.rb
547
552
  - lib/hecks/projections/bootstrap_table.rb
548
553
  - lib/hecks/projections/deploy/box.rb
554
+ - lib/hecks/projections/deploy/box/hosting.rb
555
+ - lib/hecks/projections/deploy/box/hosting_settings.rb
549
556
  - lib/hecks/projections/deploy/box/settings.rb
550
557
  - lib/hecks/projections/deploy/box/templates/box.yaml.tmpl
551
558
  - lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl
559
+ - lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part
560
+ - lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl
561
+ - lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part
562
+ - lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part
552
563
  - lib/hecks/projections/deploy/box/templates/fetch-secrets.sh
564
+ - lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl
553
565
  - lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl
554
566
  - lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl
555
567
  - lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl
556
568
  - lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl
569
+ - lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl
557
570
  - lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl
558
571
  - lib/hecks/projections/deploy/fargate.rb
559
572
  - lib/hecks/projections/deploy/fargate/assembly.rb
@@ -612,6 +625,7 @@ files:
612
625
  - lib/hecks/projections/site/edge/checks.rb
613
626
  - lib/hecks/projections/site/edge/pattern.rb
614
627
  - lib/hecks/projections/site/edge/rules.rb
628
+ - lib/hecks/projections/site/live_edge.rb
615
629
  - lib/hecks/projections/site/regions.rb
616
630
  - lib/hecks/projections/site/routes_ts.rb
617
631
  - lib/hecks/projections/site/site_cdn.rb
@@ -734,6 +748,7 @@ files:
734
748
  - lib/hecks/runtime/interpreting.rb
735
749
  - lib/hecks/runtime/invocation.rb
736
750
  - lib/hecks/runtime/loader.rb
751
+ - lib/hecks/runtime/needs.rb
737
752
  - lib/hecks/runtime/outbox.rb
738
753
  - lib/hecks/runtime/policy_interpreter.rb
739
754
  - lib/hecks/runtime/port_operation_interpreter.rb
@@ -762,17 +777,16 @@ files:
762
777
  - lib/hecks/runtime/value/field_checks.rb
763
778
  - lib/hecks/runtime/value/invariant_violation.rb
764
779
  - lib/hecks/rust_build.rb
765
- - lib/hecks/rust_build/append_optionals.rb
766
780
  - lib/hecks/rust_build/conformance.rb
767
781
  - lib/hecks/rust_build/conformance_fuzz.rb
768
782
  - lib/hecks/rust_build/coverage.rb
769
783
  - lib/hecks/rust_build/domain_name.rb
784
+ - lib/hecks/rust_build/host.rb
770
785
  - lib/hecks/rust_build/kernel_input.rb
771
786
  - lib/hecks/rust_build/native_build.rb
772
787
  - lib/hecks/rust_build/project_rust.rb
773
788
  - lib/hecks/rust_build/project_rust/codegen_run.rb
774
789
  - lib/hecks/rust_build/project_rust/root_mod.rb
775
- - lib/hecks/rust_build/rust_literal.rb
776
790
  - lib/hecks/rust_build/wasm.rb
777
791
  - lib/hecks/rust_build/wasm_browser.rb
778
792
  - lib/hecks/rust_build/write_if_changed.rb
@@ -784,8 +798,6 @@ files:
784
798
  - lib/hecks/storehouse.rb
785
799
  - lib/hecks/tenancy/bluebook/tenancy.bluebook
786
800
  - lib/hecks/tenancy/bluebook/tenancy.hecksagon
787
- - lib/hecks/three_zero.rb
788
- - lib/hecks/three_zero/forms.yml
789
801
  - lib/hecks/tickets/bluebook/tickets.bluebook
790
802
  - lib/hecks/tickets/bluebook/tickets.hecksagon
791
803
  - lib/hecks/tickets/bluebook/tickets.ports.hecksagon
@@ -814,6 +826,7 @@ files:
814
826
  - lib/hecks/tools/site_routes.rb
815
827
  - lib/hecks/tools/tail_merge.rb
816
828
  - lib/hecks/tools/tenant_provisioning.rb
829
+ - lib/hecks/tools/tools_doc.rb
817
830
  - lib/hecks/tools/translation_audit.rb
818
831
  - lib/hecks/tools/translation_scaffold.rb
819
832
  - lib/hecks/vendoring.rb
@@ -835,15 +848,12 @@ files:
835
848
  - rust/build/Cargo.toml
836
849
  - rust/build/src/build_artifact.rs
837
850
  - rust/build/src/cargo_sync.rs
838
- - rust/build/src/json.rs
839
851
  - rust/build/src/lineage_pass.rs
840
852
  - rust/build/src/main.rs
841
- - rust/build/src/optional_pass.rs
842
853
  - rust/build/src/pipeline.rs
843
854
  - rust/build/src/reserved_names.rs
844
855
  - rust/build/src/resolve.rs
845
856
  - rust/build/src/root.rs
846
- - rust/build/src/sidecars.rs
847
857
  - rust/build/src/subprocess.rs
848
858
  - rust/build/src/tmp.rs
849
859
  - rust/codegen/.cargo/config.toml
@@ -867,6 +877,7 @@ files:
867
877
  - rust/codegen/src/manifest.rs
868
878
  - rust/codegen/src/mutations.rs
869
879
  - rust/codegen/src/naming.rs
880
+ - rust/codegen/src/optional_pass.rs
870
881
  - rust/codegen/src/ports.rs
871
882
  - rust/codegen/src/prelude.rs
872
883
  - rust/codegen/src/queries.rs
@@ -876,6 +887,7 @@ files:
876
887
  - rust/codegen/src/registry.rs
877
888
  - rust/codegen/src/reserved_names.rs
878
889
  - rust/codegen/src/shared.rs
890
+ - rust/codegen/src/sidecars.rs
879
891
  - rust/codegen/src/skip_reason.rs
880
892
  - rust/codegen/src/types.rs
881
893
  - rust/host/.cargo/config.toml
@@ -969,6 +981,7 @@ files:
969
981
  - rust/parser/src/parse/hecksagon.rs
970
982
  - rust/parser/src/parse/lifecycle.rs
971
983
  - rust/parser/src/parse/mod.rs
984
+ - rust/parser/src/parse/needs.rs
972
985
  - rust/parser/src/parse/policy.rs
973
986
  - rust/parser/src/parse/process_manager.rs
974
987
  - rust/parser/src/parse/query.rs
data/lib/hecks/facade.rb DELETED
@@ -1,31 +0,0 @@
1
- # The deprecated alias shim: `Hecks::Facade` is now `Hecks::Doors` and `Doors::Surface` is
2
- # `Doors::RubyDoor`. Both old constants still resolve and warn; they are removed in
3
- # `Hecks::Doors::REMOVAL`.
4
- require_relative "doors"
5
-
6
- # The root namespace; this file holds only the deprecated aliases.
7
- module Hecks
8
- # Resolves the constants that now live in `Doors` under their old names, warning on each use.
9
- #
10
- # Extended onto `Hecks` and `Hecks::Doors`, ahead of `Module#const_missing`.
11
- module DoorsAliases
12
- # Old constant name mapped to the new one, per namespace that carried it.
13
- RENAMED = { Hecks => { Facade: "Hecks::Doors" }, Hecks::Doors => { Surface: "Hecks::Doors::RubyDoor" } }.freeze
14
-
15
- # Answers a renamed constant with its replacement and a warning; anything else
16
- # goes to the next `const_missing`.
17
- #
18
- # @param name [Symbol] the missing constant
19
- # @return [Module] the replacement module
20
- def const_missing(name)
21
- target = RENAMED.fetch(self, {})[name]
22
- return super unless target
23
-
24
- warn "[hecks] #{self}::#{name} is deprecated and is removed in #{Hecks::Doors::REMOVAL}; use #{target}"
25
- Object.const_get(target)
26
- end
27
- end
28
-
29
- extend DoorsAliases
30
- Doors.extend(DoorsAliases)
31
- end