omen 0.2.0 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0439ba57f67515df5b1685bb241e7c8d904f7039b3c349a45add84eaec5b6330'
4
- data.tar.gz: 125790550dbc3c82b2a139c4a09fc1767ea6776ce984357aa40841e397adef38
3
+ metadata.gz: c5ce1b7049f103291d3d3aabb7d02bb80271bb7dca022bb1250db4855b59a4e9
4
+ data.tar.gz: e323629870bf6d657fa4f43b0c47018eeed8af9946733f962d0e51889297ddcf
5
5
  SHA512:
6
- metadata.gz: 3123b62849d8a61bdd7264aa1eba6a13fe0bc8b5225be52cd4d5633840b5117181dca928c1ed21e6a26b54d45650f39037221cedfe8f95f7af1b5047b1523dae
7
- data.tar.gz: 6e1a011344a1ec0cb7c09657a8bdfa40159de13f4d65f892f6a23cba5ce965959224fe8c4bd4585d68d82290773035e439c0eb4ec47bf428e6f0e2814890f251
6
+ metadata.gz: ff778fcc7568f43a7b0252793443c18ab0aee925c9030dcdef9af262934eb62bfb3a9e34af21c0cb7a4f6d096cdde812fd216fc4c64008095443f44ffea3ab70
7
+ data.tar.gz: 1aea2af528735383465416dd6dcbd24770b983e630934e4ecf170e36d90120e9fbbf21e85efbd25eb6aa59860ba3c753cb66c97983f61ca5ea9785041a72d3e3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 0.2.1 - 2026-08-24
11
+
12
+ * [Fix] Declare `eastern()` over an instant as well as a stored timestamp, so a reply that
13
+ reaches for `now()` finds a function rather than `does not exist`
14
+ * [Fix] Tell Claude not to ask the database what time it is, since the prompt already says
15
+ what today is
10
16
  * [Fix] Point RubyGems at the API reference, so a gem page and a gem listing say the same
11
17
  thing about where the documentation is
12
18
 
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
 
@@ -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
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/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.1'
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo