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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +16 -1
- data/app/models/omen/answer.rb +2 -0
- data/app/models/omen/instructions.md +16 -0
- data/app/models/omen/instructions.rb +4 -1
- data/app/models/omen/question.rb +2 -0
- data/app/models/omen/reading.rb +2 -0
- data/lib/omen/distance.rb +31 -0
- data/lib/omen/engine.rb +6 -4
- data/lib/omen/inquirer.rb +1 -0
- data/lib/omen/version.rb +1 -1
- data/lib/omen.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 529ccf31849252011c372776067dfb887267dea3b8e1580a0dbb598bab0afa56
|
|
4
|
+
data.tar.gz: 7a9bb4e0ebd22451484b508b1a96e5f2ba85fcc92fcaaf62cd1f433b683b6ef5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
137
|
+
ActiveSupport.on_load(:omen_reading) { broadcasts_refreshes }
|
|
123
138
|
```
|
|
124
139
|
|
|
125
140
|
## After the first deploy
|
data/app/models/omen/answer.rb
CHANGED
|
@@ -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
|
data/app/models/omen/question.rb
CHANGED
data/app/models/omen/reading.rb
CHANGED
|
@@ -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
|
-
|
|
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/inquirer.rb
CHANGED
data/lib/omen/version.rb
CHANGED
data/lib/omen.rb
CHANGED
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.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
|