omen 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0439ba57f67515df5b1685bb241e7c8d904f7039b3c349a45add84eaec5b6330'
4
- data.tar.gz: 125790550dbc3c82b2a139c4a09fc1767ea6776ce984357aa40841e397adef38
3
+ metadata.gz: 7bff8c4c873f1cbc8923e3dbc7cebe42e282126624a56cab8d2461b8c879fb0d
4
+ data.tar.gz: e8bd747cf8234259e52e7bf5d1ab24e607dc5ba6ba09deef01f037692bd3a6f7
5
5
  SHA512:
6
- metadata.gz: 3123b62849d8a61bdd7264aa1eba6a13fe0bc8b5225be52cd4d5633840b5117181dca928c1ed21e6a26b54d45650f39037221cedfe8f95f7af1b5047b1523dae
7
- data.tar.gz: 6e1a011344a1ec0cb7c09657a8bdfa40159de13f4d65f892f6a23cba5ce965959224fe8c4bd4585d68d82290773035e439c0eb4ec47bf428e6f0e2814890f251
6
+ metadata.gz: 1c82de79367f41a2712f44bdf21d6ca715c263fbc097dae9d7dcd032ba28a152ebf4c1976b02b37f62992781f98981fab99fd49173a4925bea3664ba668cbff1
7
+ data.tar.gz: 0a2a09480ee44f60283db5db8976d6c97d6373fca2af7f6e1b2526de51052e5aba96b171ba989af70ef7bf5e9acd1fb01bbd6933cb9ec1b6cf00a10be80f0a8e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 0.2.2 - 2026-08-24
11
+
12
+ * [Fix] Run a load hook as each model loads, so a host declares `broadcasts_refreshes` and the
13
+ rest through `ActiveSupport.on_load :omen_reading` rather than by naming the class while
14
+ initializers run, which loaded Active Record before Rails was up
15
+ * [Fix] Read the adapter through the `:active_record` load hook, so a boot that never touches
16
+ the database is not the boot that loads it early
17
+
18
+ ## 0.2.1 - 2026-08-24
19
+
20
+ * [Fix] Declare `eastern()` over an instant as well as a stored timestamp, so a reply that
21
+ reaches for `now()` finds a function rather than `does not exist`
22
+ * [Fix] Tell Claude not to ask the database what time it is, since the prompt already says
23
+ what today is
10
24
  * [Fix] Point RubyGems at the API reference, so a gem page and a gem listing say the same
11
25
  thing about where the documentation is
12
26
 
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Omen
2
2
 
3
- Staff ask Claude a question about the data an app holds; Claude writes the SQL, Rails runs it.
3
+ You ask Claude a complex question about data stored by your Rails app.
4
+ Claude answers with the SQL. Rails runs it.
4
5
 
5
- Omen ships the models and the logic to talk to Claude, parse what it says back, and run the
6
- statement it wrote against a read-only connection. It ships **no controllers, routes or views**
7
- the pages belong to the app that installs it.
6
+ Omen is an engine that you too can use. You just need a Rails app running on PostgreSQL.
7
+ Omen provides the models and the logic to talk to Claude; to parse what it says; to run read-only statements.
8
8
 
9
- The rows never travel. Claude is shown the schema and writes one `SELECT`; Rails runs it and
10
- draws the answer for whoever asked. Nothing that statement returned is ever sent back.
9
+ Your data never travels. Claude is shown the schema and writes one `SELECT`; Rails runs it and
10
+ draws the answer (including encrypted attributes) for whoever asked. Nothing that statement returned is ever sent back.
11
11
 
12
12
  ## How to install
13
13
 
@@ -26,35 +26,38 @@ never crosses a breaking change.
26
26
 
27
27
  ## Requirements
28
28
 
29
- **PostgreSQL only.** This is not a gap waiting to be filled. Two of the guarantees Omen makes are
30
- Postgres features with no equivalent elsewhere: it identifies an encrypted column by the table OID
31
- and column number Postgres reports for each result column, which is what stops an alias or an
32
- expression from laundering one; and it narrows privileges for the statement it runs with
33
- `SET LOCAL ROLE`, which reverts when the transaction ends. MySQL has `SET ROLE` but nothing
34
- transaction-scoped, so a raised exception would leave a pooled connection holding the role. Omen
35
- raises at boot on any other adapter rather than running with a weaker promise.
36
-
37
- **`db/schema.rb`, in Rails' `:ruby` schema format.** The schema is what Claude is shown, so an app
38
- on `db/structure.sql` cannot use Omen. Checked at boot and raised on, because copying a
39
- `structure.sql` to that path fails silently and with teeth: the strip regexes read the Ruby DSL,
40
- so they match nothing, Omen's own tables stay in the prompt, and Claude is shown the log of every
41
- question ever asked.
42
-
43
- **Eastern time.** A stored timestamp is read through an `eastern()` function the rake task
44
- creates, and every date the prompt asks Claude to reason about is a date in `America/New_York`.
45
- An app that works in another zone has to say so in the function and rename it.
29
+ **PostgreSQL only**
30
+
31
+ <details>
32
+ <summary>This is not a gap waiting to be filled. </summary>
33
+ Two of the guarantees Omen makes are
34
+ Postgres features with no equivalent elsewhere: it identifies an encrypted column by the table OID
35
+ and column number Postgres reports for each result column, which is what stops an alias or an
36
+ expression from laundering one; and it narrows privileges for the statement it runs with
37
+ `SET LOCAL ROLE`, which reverts when the transaction ends. MySQL has `SET ROLE` but nothing
38
+ transaction-scoped, so a raised exception would leave a pooled connection holding the role. Omen
39
+ raises at boot on any other adapter rather than running with a weaker promise.
40
+ </details>
41
+
42
+ **db/schema.rb in Rails**
43
+
44
+ <details>
45
+ <summary>Only the :ruby schema is supported. </summary>
46
+ The schema is what Claude is shown, so an app
47
+ on `db/structure.sql` cannot use Omen. Checked at boot and raised on, because copying a
48
+ `structure.sql` to that path fails silently and with teeth: the strip regexes read the Ruby DSL,
49
+ so they match nothing, Omen's own tables stay in the prompt, and Claude is shown the log of every
50
+ question ever asked.
51
+ </details>
46
52
 
47
53
  ## Configuration
48
54
 
49
- Installing is three commands and, if you want to change anything, one file.
50
-
51
- ### The steps
55
+ Installing by adding to your Gemfile and running three commands in your terminal:
52
56
 
53
57
  ```sh
54
- bin/rails generate omen:install # three migrations, and an initializer you may delete
55
- bin/rails db:migrate
56
- bin/rails db:omen:grant # the role a statement runs as, and the function it reads a
57
- # timestamp through
58
+ bin/rails g omen:install # adds three migrations + an initializer you can delete
59
+ bin/rails db:migrate # creates the tables `omen_readings`, `omen_questions`, `omen_answers`
60
+ bin/rails db:omen:grant # set the read-only role statements run as
58
61
  ```
59
62
 
60
63
  `db:omen:grant` is worth running from the tasks that build a database, so a fresh one is never
@@ -71,10 +74,7 @@ granted = Rake::Task['db:omen:grant']
71
74
  end
72
75
  ```
73
76
 
74
- ### What the app around it has to provide
75
-
76
- Configuration is optional. Installation is not: four of these are the app's, and Omen cannot
77
- supply any of them.
77
+ ### Requirements
78
78
 
79
79
  - **A read-only connection role.** `connects_to database: { writing: :primary, reading: :reader }`
80
80
  on the record class, with the `reading` entry logging in as a Postgres role granted `SELECT`
@@ -95,14 +95,8 @@ writes it with each line commented out, as the list of what there is to say.
95
95
 
96
96
  | Setting | Default |
97
97
  |---|---|
98
- | `record_class` | `'ApplicationRecord'`, falling back to `ActiveRecord::Base` |
99
- | `reading_role` | `:reading` |
100
98
  | `narrow_role` | `'omen_inquirer'` |
101
- | `claude_model` | `'claude-opus-5'` |
102
- | `maximum_rows` | `100` |
103
- | `api_key` | unset, so the Anthropic SDK resolves `ANTHROPIC_API_KEY` and its wider chain |
104
99
  | `notes` | none, so the prompt says nothing about this app beyond its schema |
105
- | `schema_path` | `'db/schema.rb'` |
106
100
 
107
101
  ## What a host builds on top
108
102
 
@@ -125,7 +119,7 @@ Two things a subclass cannot reach, because the gem's own class is what a job lo
125
119
  line in the host does it:
126
120
 
127
121
  ```ruby
128
- Rails.application.config.to_prepare { Omen::Reading.broadcasts_refreshes }
122
+ ActiveSupport.on_load(:omen_reading) { broadcasts_refreshes }
129
123
  ```
130
124
 
131
125
  ## 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
@@ -33,7 +33,8 @@ day, a week or a month means whole days here: `date_trunc('day', %{eastern}(crea
33
33
  write a conversion of your own.
34
34
 
35
35
  Today is %{today}. Resolve every relative date yourself; the query has no idea what "last
36
- month" means.
36
+ month" means, and it must never ask the database what time it is -- `now()` and
37
+ `current_timestamp` are the clock of the machine, not the date above.
37
38
 
38
39
  Every type named in a `create_enum` line at the top of the schema is a Postgres enum, and the
39
40
  values it may take are listed on that line. Compare one as text, for example
@@ -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
data/lib/omen/eastern.rb CHANGED
@@ -11,12 +11,30 @@ module Omen
11
11
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
12
12
  # @return [Array<String>] the statements to run, in order.
13
13
  def self.statements(connection)
14
- name = connection.quote_table_name Omen::Instructions::EASTERN
15
- [
16
- "CREATE OR REPLACE FUNCTION #{name}(ts timestamp) RETURNS timestamp AS " \
17
- "$$ SELECT ts AT TIME ZONE 'UTC' AT TIME ZONE #{connection.quote ZONE} $$ " \
18
- 'LANGUAGE sql IMMUTABLE',
19
- ]
14
+ [ stored(connection), instant(connection) ]
20
15
  end
16
+
17
+ # A stored timestamp says nothing about its own zone, so it is named UTC and then rendered.
18
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
19
+ # @return [String] the statement declaring the function over a naked timestamp.
20
+ def self.stored(connection)
21
+ "CREATE OR REPLACE FUNCTION #{name connection}(ts timestamp) RETURNS timestamp AS " \
22
+ "$$ SELECT ts AT TIME ZONE 'UTC' AT TIME ZONE #{connection.quote ZONE} $$ " \
23
+ 'LANGUAGE sql IMMUTABLE'
24
+ end
25
+
26
+ # One conversion and not two: an instant already knows which moment it is, so naming it UTC
27
+ # first would convert it twice and answer hours out. Overloaded because Postgres will not
28
+ # cast an instant to a timestamp to resolve a call, so now() reaches neither without it.
29
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
30
+ # @return [String] the statement declaring the function over an instant.
31
+ def self.instant(connection)
32
+ "CREATE OR REPLACE FUNCTION #{name connection}(ts timestamptz) RETURNS timestamp AS " \
33
+ "$$ SELECT ts AT TIME ZONE #{connection.quote ZONE} $$ LANGUAGE sql IMMUTABLE"
34
+ end
35
+
36
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
37
+ # @return [String] the function's name, quoted.
38
+ def self.name(connection) = connection.quote_table_name Omen::Instructions::EASTERN
21
39
  end
22
40
  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/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.0'
3
+ VERSION = '0.2.2'
4
4
  end
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.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo