omen 0.3.1 → 0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +20 -14
- data/app/models/omen/instructions.md +24 -7
- data/app/models/omen/instructions.rb +8 -3
- data/lib/omen/distance.rb +1 -1
- data/lib/omen/grants.rb +2 -1
- data/lib/omen/renamed.rb +17 -0
- data/lib/omen/time_zone.rb +57 -0
- data/lib/omen/version.rb +1 -1
- data/lib/omen.rb +2 -1
- metadata +3 -2
- data/lib/omen/eastern.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd517ec9c5893a92f285613782d245ef9573bb83c8ef78378510f49a208626fc
|
|
4
|
+
data.tar.gz: 5d5f97f73bd6cf0b423f9264426522aee942df7ab4fb622e3442b75493da29c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f383e42b3c7e9fdd06f7c69c6ef7fd8563cb55f47db66583ee07c656b56bfabe6217972c63c89596db5407a96af4e8080f71e3561e7ee188a507c74d9ff28404
|
|
7
|
+
data.tar.gz: a8894e28c75abeeb62cd139ead7bf0abca83593a49c89bae50273dbe178016ef6cf915179a07355b4b7af38f4f17c06efc2e57f597e949637bf5f7528fbce8a7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 0.4.0 - 2026-08-24
|
|
11
|
+
|
|
12
|
+
* [Breaking change] Prefix every database function this gem creates, and drop the names they
|
|
13
|
+
had: `eastern` is now `omen_time_zone`, `miles_between` is `omen_miles_between`. A statement
|
|
14
|
+
stored before this names the old ones, so re-running one fails rather than answering from a
|
|
15
|
+
function nothing maintains -- an answer already drawn is unaffected, since its rows are stored
|
|
16
|
+
rather than recomputed
|
|
17
|
+
* [Feature] Create `omen_today()` and tell Claude to build every relative window on it, so a
|
|
18
|
+
statement kept and run again answers "last month" for the month it is run in rather than the
|
|
19
|
+
month it was written in. `STABLE`, and a date rather than a timestamp
|
|
20
|
+
* [Feature] Ask for a note that says a sliding window the way the statement says it, and then
|
|
21
|
+
what it comes to today, so a stored note does not stop being true on the second run
|
|
22
|
+
|
|
10
23
|
## 0.3.1 - 2026-08-24
|
|
11
24
|
|
|
12
25
|
* [Fix] Stop asserting the role attributes only a superuser may set, since `NOSUPERUSER`,
|
data/README.md
CHANGED
|
@@ -15,14 +15,15 @@ draws the answer (including encrypted attributes) for whoever asked. Nothing tha
|
|
|
15
15
|
gem install omen
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Or, in a `Gemfile`, pinned to the current
|
|
18
|
+
Or, in a `Gemfile`, pinned to the current minor while this is still below 1.0:
|
|
19
19
|
|
|
20
20
|
```ruby
|
|
21
|
-
gem 'omen', '~> 0.
|
|
21
|
+
gem 'omen', '~> 0.4.0'
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Omen follows [Semantic Versioning](https://semver.org)
|
|
25
|
-
|
|
24
|
+
Omen follows [Semantic Versioning](https://semver.org) from 1.0 onwards. Until then a release may
|
|
25
|
+
break whatever it likes, so the pin stops short of the next minor: `~> 0.4.0` takes every fix in
|
|
26
|
+
0.4 and nothing beyond it.
|
|
26
27
|
|
|
27
28
|
## Requirements
|
|
28
29
|
|
|
@@ -50,18 +51,23 @@ never crosses a breaking change.
|
|
|
50
51
|
question ever asked.
|
|
51
52
|
</details>
|
|
52
53
|
|
|
53
|
-
**
|
|
54
|
+
**Three database functions, all prefixed**
|
|
54
55
|
|
|
55
56
|
<details>
|
|
56
|
-
<summary>A timestamp and a distance
|
|
57
|
-
`db:omen:grant` creates
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
<summary>A timestamp, a day and a distance go through a function, never an expression. </summary>
|
|
58
|
+
`db:omen:grant` creates all three, each named `omen_` so that none of them can take a name an
|
|
59
|
+
app wanted for itself -- `today` especially. `omen_time_zone()` hands a stored timestamp back in
|
|
60
|
+
the zone the company works in, so every date means the same whole days. `omen_today()` answers
|
|
61
|
+
what day it is there, and the prompt builds every relative window on it, so a statement that is
|
|
62
|
+
kept and run again answers "last month" for the month it is run in rather than the month it was
|
|
63
|
+
written in. `omen_miles_between(lat1, lng1, lat2, lng2)` answers a great-circle distance in
|
|
64
|
+
miles. The prompt names each and forbids writing any of them by hand: a conversion assembled per
|
|
65
|
+
query drifts, and a great-circle expression runs to a dozen nested calls that a reply balances
|
|
66
|
+
by hand and gets wrong. The first and last are `IMMUTABLE`; `omen_today()` is `STABLE`, because
|
|
67
|
+
it reads the clock and an immutable function of the clock may be folded to a constant -- which
|
|
68
|
+
is exactly the sliding this one exists to keep. All are executable by anyone, so none needs a
|
|
69
|
+
grant. An app in another zone renames the first two; an app whose tables carry no coordinates
|
|
70
|
+
never calls the last. None can be a migration: Rails' `:ruby` schema format dumps no functions,
|
|
65
71
|
so `db:schema:load` would drop one a migration had made.
|
|
66
72
|
</details>
|
|
67
73
|
|
|
@@ -31,19 +31,32 @@ have made it, and switching quote style to avoid an apostrophe is how you make i
|
|
|
31
31
|
|
|
32
32
|
## Reading the schema
|
|
33
33
|
|
|
34
|
-
Timestamps are stored in UTC, and `%{
|
|
34
|
+
Timestamps are stored in UTC, and `%{zone_fn}()` is the one way to read one: it hands the same
|
|
35
35
|
moment back in the zone the company works in. Wrap every timestamp you touch in it -- in a
|
|
36
36
|
`WHERE`, in an `ORDER BY`, in a `GROUP BY`, and in a column you return -- so a question about a
|
|
37
|
-
day, a week or a month means whole days here: `date_trunc('day', %{
|
|
37
|
+
day, a week or a month means whole days here: `date_trunc('day', %{zone_fn}(created_at))`. Never
|
|
38
38
|
write a conversion of your own.
|
|
39
39
|
|
|
40
|
-
Today is %{today}
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
Today is %{today}, and `%{today_fn}()` is that same day asked of the database. Use it for every
|
|
41
|
+
window a question describes in relation to now:
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
WHERE %{zone_fn}(created_at) >= date_trunc('month', %{today_fn}()) - interval '1 month'
|
|
45
|
+
AND %{zone_fn}(created_at) < date_trunc('month', %{today_fn}())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Write it that way rather than working the dates out and putting them in, because a statement is
|
|
49
|
+
kept and run again: one carrying `'2026-08-01'` answers a question nobody asked the second time,
|
|
50
|
+
where one carrying `%{today_fn}()` still answers "last month" whenever it is run. A window a
|
|
51
|
+
question names outright -- "in July 2026", "since the 3rd" -- is not relative to now, and keeps
|
|
52
|
+
its literal dates.
|
|
53
|
+
|
|
54
|
+
Never `now()`, `current_date` or `current_timestamp`: those are the machine's clock in the
|
|
55
|
+
machine's zone. `%{today_fn}()` is the company's day, and it is a date, so it needs no truncating.
|
|
43
56
|
|
|
44
57
|
Where a table carries coordinates, the distance between two points in miles is
|
|
45
|
-
`%{
|
|
46
|
-
`WHERE %{
|
|
58
|
+
`%{miles_fn}(lat1, lng1, lat2, lng2)`, so a radius reads
|
|
59
|
+
`WHERE %{miles_fn}(l.lat, l.lng, u.lat, u.lng) <= 2`. Never write the trigonometry yourself: a
|
|
47
60
|
great-circle expression built by hand runs to a dozen nested calls, and one bracket out of place
|
|
48
61
|
either refuses the statement or, worse, measures something else and says nothing about it.
|
|
49
62
|
|
|
@@ -102,6 +115,10 @@ one of those.
|
|
|
102
115
|
Otherwise keep the note to a sentence or two: what the query returns, and any assumption you
|
|
103
116
|
made. These are colleagues reading quickly, not a report.
|
|
104
117
|
|
|
118
|
+
Say a sliding window the way the statement says it, and then what it comes to today: "in the
|
|
119
|
+
previous calendar month (July 2026)". A note reading "in July 2026" alone stops being true the
|
|
120
|
+
moment the statement is run again.
|
|
121
|
+
|
|
105
122
|
## The schema
|
|
106
123
|
|
|
107
124
|
```ruby
|
|
@@ -4,10 +4,14 @@ class Omen::Instructions
|
|
|
4
4
|
PROSE = File.expand_path 'instructions.md', __dir__
|
|
5
5
|
|
|
6
6
|
# The database function a stored timestamp is read through, created by the gem's rake task.
|
|
7
|
-
|
|
7
|
+
# Every one of these is prefixed, since `today` especially is a name a host may want itself.
|
|
8
|
+
TIME_ZONE = 'omen_time_zone'
|
|
9
|
+
|
|
10
|
+
# The database function that answers what day it is where the company works.
|
|
11
|
+
TODAY = 'omen_today'
|
|
8
12
|
|
|
9
13
|
# The database function a distance in miles is measured with, created by the same task.
|
|
10
|
-
MILES = '
|
|
14
|
+
MILES = 'omen_miles_between'
|
|
11
15
|
|
|
12
16
|
# The one shape a reply may take: both keys required, and no others admitted.
|
|
13
17
|
ANSWER = {
|
|
@@ -36,7 +40,8 @@ class Omen::Instructions
|
|
|
36
40
|
# Today's date is said out loud because "last month" is Claude's to resolve, and it has no clock.
|
|
37
41
|
# @return [String] the prose, with the schema, the subclasses and the host's notes filled in.
|
|
38
42
|
def text
|
|
39
|
-
format File.read(PROSE), today: Date.current.to_fs(:long),
|
|
43
|
+
format File.read(PROSE), today: Date.current.to_fs(:long), zone_fn: TIME_ZONE,
|
|
44
|
+
today_fn: TODAY, miles_fn: MILES,
|
|
40
45
|
schema: schema, types: types, readable: readable, refused: refused,
|
|
41
46
|
notes: Omen.config.notes
|
|
42
47
|
end
|
data/lib/omen/distance.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Omen
|
|
2
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::
|
|
3
|
+
# than by a migration, for the reason Omen::TimeZone is: Rails' :ruby schema format dumps no
|
|
4
4
|
# functions, so db:schema:load would drop one a migration had made.
|
|
5
5
|
module Distance
|
|
6
6
|
# The earth's mean radius in miles, which is what makes the answer miles.
|
data/lib/omen/grants.rb
CHANGED
|
@@ -21,7 +21,8 @@ module Omen
|
|
|
21
21
|
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO #{role}",
|
|
22
22
|
*members.map { |member| "GRANT #{role} TO #{connection.quote_table_name member}" },
|
|
23
23
|
*revoked(connection, role),
|
|
24
|
-
*Omen::
|
|
24
|
+
*Omen::Renamed.statements,
|
|
25
|
+
*Omen::TimeZone.statements(connection),
|
|
25
26
|
*Omen::Distance.statements(connection),
|
|
26
27
|
]
|
|
27
28
|
end
|
data/lib/omen/renamed.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Omen
|
|
2
|
+
# The names these functions had before 1.0, dropped so a database matches the gem installed in
|
|
3
|
+
# it. A statement stored before the rename names them, and dropping them makes re-running one
|
|
4
|
+
# fail where leaving them would have it quietly answer -- from a function nothing maintains any
|
|
5
|
+
# more. An answer already drawn is unaffected: its rows are stored, not recomputed.
|
|
6
|
+
#
|
|
7
|
+
# Delete this file once no database still has them.
|
|
8
|
+
module Renamed
|
|
9
|
+
# Signatures rather than names, since a function is dropped by the arguments it takes.
|
|
10
|
+
BEFORE = [ 'eastern(timestamp)', 'eastern(timestamptz)',
|
|
11
|
+
'miles_between(double precision, double precision, double precision, ' \
|
|
12
|
+
'double precision)', ]
|
|
13
|
+
|
|
14
|
+
# @return [Array<String>] one DROP per function this gem used to create.
|
|
15
|
+
def self.statements = BEFORE.map { |signature| "DROP FUNCTION IF EXISTS #{signature}" }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Omen
|
|
2
|
+
# The zone the company works in, and the two questions a reading asks about it: what a stored
|
|
3
|
+
# timestamp says here, and what day it is here. Created by the rake task rather than by a
|
|
4
|
+
# migration, because Rails' :ruby schema format dumps no functions, so db:schema:load would
|
|
5
|
+
# drop one a migration had made -- and the format cannot become :sql, since db/schema.rb is
|
|
6
|
+
# what Claude is shown.
|
|
7
|
+
module TimeZone
|
|
8
|
+
# The zone as Postgres names one: Time.zone.name is not a name it takes.
|
|
9
|
+
ZONE = 'America/New_York'
|
|
10
|
+
|
|
11
|
+
# DDL, which Active Record has no expression for, and not a query.
|
|
12
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
13
|
+
# @return [Array<String>] the statements to run, in order.
|
|
14
|
+
def self.statements(connection) = [ stored(connection), instant(connection), today(connection) ]
|
|
15
|
+
|
|
16
|
+
# A stored timestamp says nothing about its own zone, so it is named UTC and then rendered.
|
|
17
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
18
|
+
# @return [String] the statement declaring the function over a naked timestamp.
|
|
19
|
+
def self.stored(connection)
|
|
20
|
+
"CREATE OR REPLACE FUNCTION #{name connection}(ts timestamp) RETURNS timestamp AS " \
|
|
21
|
+
"$$ SELECT ts AT TIME ZONE 'UTC' AT TIME ZONE #{connection.quote ZONE} $$ " \
|
|
22
|
+
'LANGUAGE sql IMMUTABLE'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# One conversion and not two: an instant already knows which moment it is, so naming it UTC
|
|
26
|
+
# first would convert it twice and answer hours out. Overloaded because Postgres will not
|
|
27
|
+
# cast an instant to a timestamp to resolve a call, so now() reaches neither without it.
|
|
28
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
29
|
+
# @return [String] the statement declaring the function over an instant.
|
|
30
|
+
def self.instant(connection)
|
|
31
|
+
"CREATE OR REPLACE FUNCTION #{name connection}(ts timestamptz) RETURNS timestamp AS " \
|
|
32
|
+
"$$ SELECT ts AT TIME ZONE #{connection.quote ZONE} $$ LANGUAGE sql IMMUTABLE"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# What makes a window slide: a statement saying `date_trunc('month', omen_today())` answers
|
|
36
|
+
# for whichever month it is run in, where a literal answers for the month it was written in.
|
|
37
|
+
#
|
|
38
|
+
# STABLE and not IMMUTABLE, unlike its neighbours, which are pure functions of what they are
|
|
39
|
+
# given. This one reads the clock: marked immutable, Postgres is entitled to fold it to a
|
|
40
|
+
# constant, and a window would stop moving in the way this function exists to prevent. Stable
|
|
41
|
+
# is also what has it evaluated once per statement, so a query naming it five times cannot
|
|
42
|
+
# straddle midnight.
|
|
43
|
+
#
|
|
44
|
+
# A date and not a timestamp, so a window cannot silently mean "up to the current hour".
|
|
45
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
46
|
+
# @return [String] the statement declaring the function.
|
|
47
|
+
def self.today(connection)
|
|
48
|
+
"CREATE OR REPLACE FUNCTION #{connection.quote_table_name Omen::Instructions::TODAY}() " \
|
|
49
|
+
"RETURNS date AS $$ SELECT (now() AT TIME ZONE #{connection.quote ZONE})::date $$ " \
|
|
50
|
+
'LANGUAGE sql STABLE'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
54
|
+
# @return [String] the function's name, quoted.
|
|
55
|
+
def self.name(connection) = connection.quote_table_name Omen::Instructions::TIME_ZONE
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/omen/version.rb
CHANGED
data/lib/omen.rb
CHANGED
|
@@ -4,8 +4,9 @@ require 'anthropic'
|
|
|
4
4
|
require 'omen/attributes'
|
|
5
5
|
require 'omen/config'
|
|
6
6
|
require 'omen/distance'
|
|
7
|
-
require 'omen/eastern'
|
|
8
7
|
require 'omen/grants'
|
|
8
|
+
require 'omen/renamed'
|
|
9
|
+
require 'omen/time_zone'
|
|
9
10
|
require 'omen/inquirer'
|
|
10
11
|
require 'omen/requirements'
|
|
11
12
|
require 'omen/version'
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -100,12 +100,13 @@ files:
|
|
|
100
100
|
- lib/omen/attributes.rb
|
|
101
101
|
- lib/omen/config.rb
|
|
102
102
|
- lib/omen/distance.rb
|
|
103
|
-
- lib/omen/eastern.rb
|
|
104
103
|
- lib/omen/engine.rb
|
|
105
104
|
- lib/omen/grants.rb
|
|
106
105
|
- lib/omen/inquirer.rb
|
|
106
|
+
- lib/omen/renamed.rb
|
|
107
107
|
- lib/omen/requirements.rb
|
|
108
108
|
- lib/omen/stubs.rb
|
|
109
|
+
- lib/omen/time_zone.rb
|
|
109
110
|
- lib/omen/version.rb
|
|
110
111
|
- lib/tasks/omen.rake
|
|
111
112
|
homepage: https://github.com/claudiob/omen
|
data/lib/omen/eastern.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
module Omen
|
|
2
|
-
# The database function a reading's SQL reads every timestamp through. Created by the rake
|
|
3
|
-
# task rather than by a migration, because Rails' :ruby schema format dumps no functions, so
|
|
4
|
-
# db:schema:load would drop one a migration had made -- and the format cannot become :sql,
|
|
5
|
-
# since db/schema.rb is what Claude is shown.
|
|
6
|
-
module Eastern
|
|
7
|
-
# The zone the company works in, as Postgres names one: Time.zone.name is not one it takes.
|
|
8
|
-
ZONE = 'America/New_York'
|
|
9
|
-
|
|
10
|
-
# DDL, which Active Record has no expression for, and not a query.
|
|
11
|
-
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
12
|
-
# @return [Array<String>] the statements to run, in order.
|
|
13
|
-
def self.statements(connection)
|
|
14
|
-
[ stored(connection), instant(connection) ]
|
|
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
|
|
39
|
-
end
|
|
40
|
-
end
|