omen 0.2.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5ce1b7049f103291d3d3aabb7d02bb80271bb7dca022bb1250db4855b59a4e9
4
- data.tar.gz: e323629870bf6d657fa4f43b0c47018eeed8af9946733f962d0e51889297ddcf
3
+ metadata.gz: 529ccf31849252011c372776067dfb887267dea3b8e1580a0dbb598bab0afa56
4
+ data.tar.gz: 7a9bb4e0ebd22451484b508b1a96e5f2ba85fcc92fcaaf62cd1f433b683b6ef5
5
5
  SHA512:
6
- metadata.gz: ff778fcc7568f43a7b0252793443c18ab0aee925c9030dcdef9af262934eb62bfb3a9e34af21c0cb7a4f6d096cdde812fd216fc4c64008095443f44ffea3ab70
7
- data.tar.gz: 1aea2af528735383465416dd6dcbd24770b983e630934e4ecf170e36d90120e9fbbf21e85efbd25eb6aa59860ba3c753cb66c97983f61ca5ea9785041a72d3e3
6
+ metadata.gz: b9d59820ced246e19969a0ff0dbea02b29feb1bc85be637522066ec49d3a15b8e11c83d0aca97397a05235ab1cd7413dc3553bba3b8d17102933fae40c18d549
7
+ data.tar.gz: bdcda330b4d7e03ef0b1fa6e2586b8c35dee10101af54600e5cdde47ca1f23b0ed06825e59b3d43ce7559f906aab03de186a2ae75d19396bfae8e481e6670a0e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 0.3.0 - 2026-08-24
11
+
12
+ * [Feature] Create a `miles_between(lat1, lng1, lat2, lng2)` function and name it in the prompt,
13
+ so a radius is four arguments rather than a dozen nested trigonometric calls that a reply
14
+ balances by hand -- and gets wrong
15
+ * [Fix] Tell Claude that text is single-quoted and an apostrophe inside it is doubled, since a
16
+ double-quoted literal is read as a column name and refuses the whole statement
17
+ * [Fix] Tell Claude to declare a `combine` where an encrypted value belongs inside a sentence,
18
+ rather than returning the pieces as columns for the page to draw apart
19
+
20
+ ## 0.2.2 - 2026-08-24
21
+
22
+ * [Fix] Run a load hook as each model loads, so a host declares `broadcasts_refreshes` and the
23
+ rest through `ActiveSupport.on_load :omen_reading` rather than by naming the class while
24
+ initializers run, which loaded Active Record before Rails was up
25
+ * [Fix] Read the adapter through the `:active_record` load hook, so a boot that never touches
26
+ the database is not the boot that loads it early
27
+
10
28
  ## 0.2.1 - 2026-08-24
11
29
 
12
30
  * [Fix] Declare `eastern()` over an instant as well as a stored timestamp, so a reply that
data/README.md CHANGED
@@ -50,6 +50,21 @@ never crosses a breaking change.
50
50
  question ever asked.
51
51
  </details>
52
52
 
53
+ **Two database functions**
54
+
55
+ <details>
56
+ <summary>A timestamp and a distance are read through a function, never an expression. </summary>
57
+ `db:omen:grant` creates both. `eastern()` hands a stored timestamp back in the zone the company
58
+ works in, so every date in the prompt means the same whole days, and
59
+ `miles_between(lat1, lng1, lat2, lng2)` answers a great-circle distance in miles. The prompt
60
+ names each and forbids writing either by hand: a conversion assembled per query drifts, and a
61
+ great-circle expression runs to a dozen nested calls that a reply balances by hand and gets
62
+ wrong. Both are `LANGUAGE sql IMMUTABLE` and executable by anyone, so neither needs a grant.
63
+ An app in another zone renames the first; an app whose tables carry no coordinates simply never
64
+ calls the second. Neither can be a migration: Rails' `:ruby` schema format dumps no functions,
65
+ so `db:schema:load` would drop one a migration had made.
66
+ </details>
67
+
53
68
  ## Configuration
54
69
 
55
70
  Installing by adding to your Gemfile and running three commands in your terminal:
@@ -119,7 +134,7 @@ Two things a subclass cannot reach, because the gem's own class is what a job lo
119
134
  line in the host does it:
120
135
 
121
136
  ```ruby
122
- Rails.application.config.to_prepare { Omen::Reading.broadcasts_refreshes }
137
+ ActiveSupport.on_load(:omen_reading) { broadcasts_refreshes }
123
138
  ```
124
139
 
125
140
  ## After the first deploy
@@ -35,3 +35,5 @@ class Omen::Answer < Omen.config.record
35
35
  # @return [Boolean] whether the answer ran past what is shown.
36
36
  def truncated? = result.size > Omen.config.maximum_rows
37
37
  end
38
+
39
+ ActiveSupport.run_load_hooks :omen_answer, Omen::Answer
@@ -24,6 +24,11 @@ comes back however you write it. A `WITH` clause is fine.
24
24
  Prefer `count(*)`, `group by` and aggregates over returning raw rows: a question about how many
25
25
  or about which is most is answered better by ten rows than by a thousand.
26
26
 
27
+ Text goes in single quotes, and an apostrophe inside it is written twice: `'it''s'`. Double
28
+ quotes name a column, so `"it's here"` is read as the name of a column, no such column is found
29
+ and the whole statement is refused. It is the one mistake that still looks like text after you
30
+ have made it, and switching quote style to avoid an apostrophe is how you make it.
31
+
27
32
  ## Reading the schema
28
33
 
29
34
  Timestamps are stored in UTC, and `%{eastern}()` is the one way to read one: it hands the same
@@ -36,6 +41,12 @@ Today is %{today}. Resolve every relative date yourself; the query has no idea w
36
41
  month" means, and it must never ask the database what time it is -- `now()` and
37
42
  `current_timestamp` are the clock of the machine, not the date above.
38
43
 
44
+ Where a table carries coordinates, the distance between two points in miles is
45
+ `%{miles}(lat1, lng1, lat2, lng2)`, so a radius reads
46
+ `WHERE %{miles}(l.lat, l.lng, u.lat, u.lng) <= 2`. Never write the trigonometry yourself: a
47
+ great-circle expression built by hand runs to a dozen nested calls, and one bracket out of place
48
+ either refuses the statement or, worse, measures something else and says nothing about it.
49
+
39
50
  Every type named in a `create_enum` line at the top of the schema is a Postgres enum, and the
40
51
  values it may take are listed on that line. Compare one as text, for example
41
52
  `WHERE status::text = 'fulfilled'`.
@@ -55,6 +66,11 @@ decrypting, and the page draws one column under the name you gave, in place of i
55
66
  entry of `parts` has to be a header your query really returns. Where nothing needs joining,
56
67
  `combine` is `[]` -- and never explain a join in `note` instead of declaring it.
57
68
 
69
+ Declare it the same way where the value belongs inside a sentence you are building: return the
70
+ text before it, the column itself, and the text after it as three columns, and join them with
71
+ `"separator": ""`. What you must not do is leave the parts as separate columns and let the page
72
+ draw them apart, which is what happens when you work around the ciphertext instead of saying so.
73
+
58
74
  These columns are stored encrypted as well, and no page ever reads one back -- either the name
59
75
  reads as a credential's, or the value is encrypted in a way no two writes of it agree on -- so
60
76
  do not select them: %{refused}.
@@ -6,6 +6,9 @@ class Omen::Instructions
6
6
  # The database function a stored timestamp is read through, created by the gem's rake task.
7
7
  EASTERN = 'eastern'
8
8
 
9
+ # The database function a distance in miles is measured with, created by the same task.
10
+ MILES = 'miles_between'
11
+
9
12
  # The one shape a reply may take: both keys required, and no others admitted.
10
13
  ANSWER = {
11
14
  type: 'object', additionalProperties: false, required: %w[ sql note combine ],
@@ -33,7 +36,7 @@ class Omen::Instructions
33
36
  # Today's date is said out loud because "last month" is Claude's to resolve, and it has no clock.
34
37
  # @return [String] the prose, with the schema, the subclasses and the host's notes filled in.
35
38
  def text
36
- format File.read(PROSE), today: Date.current.to_fs(:long), eastern: EASTERN,
39
+ format File.read(PROSE), today: Date.current.to_fs(:long), eastern: EASTERN, miles: MILES,
37
40
  schema: schema, types: types, readable: readable, refused: refused,
38
41
  notes: Omen.config.notes
39
42
  end
@@ -11,3 +11,5 @@ class Omen::Question < Omen.config.record
11
11
  # @return [String] the side of the conversation this was said on, in the words the API uses.
12
12
  def role = 'user'
13
13
  end
14
+
15
+ ActiveSupport.run_load_hooks :omen_question, Omen::Question
@@ -16,3 +16,5 @@ class Omen::Reading < Omen.config.record
16
16
  # @return [String] the default representation (used in views).
17
17
  def to_s = questions.first&.text.to_s.truncate 80
18
18
  end
19
+
20
+ ActiveSupport.run_load_hooks :omen_reading, Omen::Reading
@@ -0,0 +1,31 @@
1
+ module Omen
2
+ # The database function a reading measures a distance with. Created by the rake task rather
3
+ # than by a migration, for the reason Omen::Eastern is: Rails' :ruby schema format dumps no
4
+ # functions, so db:schema:load would drop one a migration had made.
5
+ module Distance
6
+ # The earth's mean radius in miles, which is what makes the answer miles.
7
+ RADIUS = 3958.7613
8
+
9
+ # DDL, which Active Record has no expression for, and not a query.
10
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
11
+ # @return [Array<String>] the statements to run, in order.
12
+ def self.statements(connection) = [ haversine(connection) ]
13
+
14
+ # Declared over double precision, which numeric, real and integer all cast to implicitly,
15
+ # so a host's own column type does not have to be guessed at. Any argument NULL and the
16
+ # answer is NULL, the way a distance to nowhere ought to read.
17
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
18
+ # @return [String] the statement declaring the function.
19
+ def self.haversine(connection)
20
+ "CREATE OR REPLACE FUNCTION #{name connection}(lat1 double precision, " \
21
+ 'lng1 double precision, lat2 double precision, lng2 double precision) ' \
22
+ "RETURNS double precision AS $$ SELECT 2 * #{RADIUS} * asin(sqrt(" \
23
+ 'power(sin(radians(lat2 - lat1) / 2), 2) + cos(radians(lat1)) * cos(radians(lat2)) ' \
24
+ '* power(sin(radians(lng2 - lng1) / 2), 2))) $$ LANGUAGE sql IMMUTABLE'
25
+ end
26
+
27
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
28
+ # @return [String] the function's name, quoted.
29
+ def self.name(connection) = connection.quote_table_name Omen::Instructions::MILES
30
+ end
31
+ end
data/lib/omen/engine.rb CHANGED
@@ -7,10 +7,12 @@ module Omen
7
7
  # initializer has already said where its schema is. The declared format rather than the
8
8
  # applied one, so nothing here depends on which after_initialize hook ran first.
9
9
  config.after_initialize do
10
- configured = ActiveRecord::Base.configurations.configs_for env_name: Rails.env,
11
- name: 'primary'
12
- Omen::Requirements.met adapter: configured&.adapter, schema: Omen.config.schema,
13
- schema_format: Rails.application.config.active_record.schema_format
10
+ ActiveSupport.on_load :active_record do
11
+ configured = ActiveRecord::Base.configurations.configs_for env_name: Rails.env,
12
+ name: 'primary'
13
+ Omen::Requirements.met adapter: configured&.adapter, schema: Omen.config.schema,
14
+ schema_format: Rails.application.config.active_record.schema_format
15
+ end
14
16
  end
15
17
  end
16
18
  end
data/lib/omen/inquirer.rb CHANGED
@@ -75,6 +75,7 @@ module Omen
75
75
  *members.map { |member| "GRANT #{role} TO #{connection.quote_table_name member}" },
76
76
  *revoked(connection, role),
77
77
  *Eastern.statements(connection),
78
+ *Distance.statements(connection),
78
79
  ]
79
80
  end
80
81
 
data/lib/omen/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Omen
2
2
  # The version of this gem, as RubyGems knows it.
3
- VERSION = '0.2.1'
3
+ VERSION = '0.3.0'
4
4
  end
data/lib/omen.rb CHANGED
@@ -2,6 +2,7 @@ require 'active_job/performs'
2
2
  require 'anthropic'
3
3
 
4
4
  require 'omen/config'
5
+ require 'omen/distance'
5
6
  require 'omen/eastern'
6
7
  require 'omen/inquirer'
7
8
  require 'omen/requirements'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
@@ -98,6 +98,7 @@ files:
98
98
  - lib/generators/omen/install/templates/omen.rb
99
99
  - lib/omen.rb
100
100
  - lib/omen/config.rb
101
+ - lib/omen/distance.rb
101
102
  - lib/omen/eastern.rb
102
103
  - lib/omen/engine.rb
103
104
  - lib/omen/inquirer.rb