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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +35 -41
- data/app/models/omen/answer.rb +2 -0
- data/app/models/omen/instructions.md +2 -1
- data/app/models/omen/question.rb +2 -0
- data/app/models/omen/reading.rb +2 -0
- data/lib/omen/eastern.rb +24 -6
- data/lib/omen/engine.rb +6 -4
- data/lib/omen/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7bff8c4c873f1cbc8923e3dbc7cebe42e282126624a56cab8d2461b8c879fb0d
|
|
4
|
+
data.tar.gz: e8bd747cf8234259e52e7bf5d1ab24e607dc5ba6ba09deef01f037692bd3a6f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
55
|
-
bin/rails db:migrate
|
|
56
|
-
bin/rails db:omen:grant
|
|
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
|
-
###
|
|
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
|
-
|
|
122
|
+
ActiveSupport.on_load(:omen_reading) { broadcasts_refreshes }
|
|
129
123
|
```
|
|
130
124
|
|
|
131
125
|
## After the first deploy
|
data/app/models/omen/answer.rb
CHANGED
|
@@ -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/app/models/omen/question.rb
CHANGED
data/app/models/omen/reading.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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