namo 0.24.0 → 0.25.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e1ecaf29846a7876a59dae0d82c6c9d4e1fddd84b0c6a101941214ed6544517
4
- data.tar.gz: 4245b13c3485a504ab5c365803de282cb508579b2ca9aec0799df0f1ce653d68
3
+ metadata.gz: cf81d723c5723d7737dd544596abbe5b69323f9f027a1aac7caef2cb857ebf5a
4
+ data.tar.gz: 6b1130eb34b16920641e594b52aaff58e9f275ac2a26b0089b21f62a6d8f6630
5
5
  SHA512:
6
- metadata.gz: 41156dc96b98f59f8ff0dcf0d78f3b80ed895f402e11af9a53ba2e6cdb13d8bf39f1a38f48f2457bfe8bcf9082d146b73dafa4663e068816b2339d6888fd25af
7
- data.tar.gz: e9c1f033e17fd2b10ffab984ea6349b7641de8abf0f04f23f85c893793242abd9e795847244855ad18b8c3f74b5876cce16dd212b2d1d2a6d327998657c9a4dc
6
+ metadata.gz: fb7c989aeb34877ed56e83190bf947217370103420c6f5e5186a88316217f35c4c801d0ed1864bd0105129d3391c99a635ed756c1050bb57a6ce33c4fbffa0e5
7
+ data.tar.gz: da0f8fd20ccfa3f33b860059f33a8e65ddfed9bc157c02c1dd73389c3b907b8f4dbe82eeaf89f6a5e28a45274b74d8d03121ea7bdee9855d772242e4c50f8683
data/CHANGELOG CHANGED
@@ -1,6 +1,29 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260703
5
+ 0.25.0: + Namo#attach! — attach a formulary, evicting any data column whose name a formulary method collides with, rather than raising.
6
+
7
+ attach! evicts the colliding data columns first (as []='s proc-over-data assignment does), then attaches — the bang is the explicit consent that attach's raise withholds by default. << stays guarded: a Module still routes to attach.
8
+
9
+ 1. ~ Namo#attach — extract the collision set into a new private attach_collisions(modul) (modul.public_instance_methods(false) & data_dimensions); behaviour unchanged, still raises ArgumentError naming the collisions when the set is non-empty.
10
+ 2, + Namo#attach! — evicts each colliding data column (@data.each{|row| row.delete(name)} per colliding name, the column membership already confirmed by attach_collisions so no key-guard is needed), then forwards to @formulae.attach and returns self. The eviction mirrors []='s proc-branch clear. + private attach_collisions(modul). << is unchanged: a Module still routes to attach (the guarded form), so the operator stays safe and attach! is reached only by explicit call — one safe operator, one explicit forceful method, matching []= having no operator shortcut for its own eviction.
11
+ 3. ~ test/namo_test.rb: + an "#attach!" describe — attach! attaches as attach does for a non-colliding module (returns self, methods resolve); attach! evicts a colliding data column and registers the formulary method in its place, with all access paths (values, Row, selection) agreeing on the formula afterward and the name appearing once in derived_dimensions, not in data_dimensions; attach! over several colliding columns evicts all of them; attach! on a vanilla collision-free module leaves the data untouched; the existing attach collision-raise tests are unchanged, pinning that the guarded form still raises where attach! evicts.
12
+ 4. ~ README.md: ~ the Formularies section — + attach! as the forceful sibling of attach, with the materialised-snapshot refresh as the motivating case and the bang-is-consent rationale; + the self-eviction-recurses caveat (a formulary method that reads a column its own name evicts recurses on access, exclusive storage leaving no shadowed value behind the formula — the one way attach!'s eviction differs in feel from []='s); note that << stays the guarded form.
13
+ 5. ~ ROADMAP.md: + the 0.25.0 release entry; current-state bumped to 0.25.0.
14
+ 6. ~ Namo::VERSION: /0.24.1/0.25.0/
15
+
16
+ 20260629
17
+ 0.24.1: ~ Namo#add_row — guard row-append against a data/formula name collision.
18
+
19
+ 0.24.0 left row-append (the Hash/Row arms of <<) unguarded, on the reasoning that the data/formula exclusivity invariant was []='s to keep. That reasoning had a hole: when an appended row carries a key matching an existing formula name and lands as @data.first (e.g. the first row streamed into an empty formulary-bearing Namo), bulk values reads the raw datum (data_dimensions keys off @data.first.keys) while per-Row access derives the formula (Row#[] checks @formulae first) — the two access paths disagree, the precise split the invariant exists to prevent. So add_row now raises rather than admitting the collision, symmetric with attach's guard: a row is a bulk append you didn't name column-by-column, so refusing is safer than []='s call-site eviction.
20
+
21
+ 1. ~ lib/namo.rb: ~ private add_row(row) — + a guard: collisions = row.keys & derived_dimensions; raise ArgumentError naming them unless empty, before appending. A non-colliding row appends and returns self as before. << is otherwise unchanged: Module→attach, Row→add_row(row.to_h), Hash→add_row(row), else→TypeError.
22
+ 2. ~ test/namo_test.rb: + the "#<<" describe gains two guard cases — an appended Hash whose keys collide with a formula raises ArgumentError naming the collision, and an appended Row likewise. The existing non-colliding row/chain tests are unchanged.
23
+ 3. ~ README.md: ~ the Formularies << note — the row arm now states the guard (a row colliding with a formula name raises, symmetric with attach), replacing the 0.24.0 "surfaces live per existing precedence" wording, which held only when the colliding key was not @data.first.
24
+ 4. ~ ROADMAP.md: + the 0.24.1 release entry; current-state bumped to 0.24.1; the 0.24.0 "not guarded" note annotated as guarded at 0.24.1.
25
+ 5. ~ Namo::VERSION: /0.24.0/0.24.1/
26
+
4
27
  20260629
5
28
  0.24.0: ~ Namo#<<, Namo::Collection#<< — widen from a formulary-only alias into a polymorphic "append a constituent" operator.
6
29
 
data/README.md CHANGED
@@ -751,7 +751,7 @@ flows.values(:cum_delta) # => [20, -20, -20]
751
751
  flows.derived_dimensions # => [:signed_volume, :cum_delta]
752
752
  ```
753
753
 
754
- `<<` appends a *constituent*. For a base Namo the constituents are formularies and rows: a **module** attaches (the operator form of `attach`, so `flows << OrderFlow` reads the same as `flows.attach(OrderFlow)`); a **Hash** appends as a data row; a **Row** (one drawn from another Namo) appends its underlying hash as a row. It returns the Namo, so the arms chain freely: `flows << OrderFlow << {date: 4, buys: 10, sells: 5} << Scoring`. Two things `<<` deliberately does not take: a bare callable (formula *registration* stays with `[]=`, which dispatches callable-vs-scalar and enforces data/formula exclusivity) and a whole Namo (combining collections is `+`'s job) — both raise. A bare Hash is therefore always a row, never a body of formulae. Appending a row is a data mutation that does not pass through `[]=`, so it does not trigger formula-eviction; a row carrying a formula-named key is your data, surfacing live through the formula on access per the usual precedence.
754
+ `<<` appends a *constituent*. For a base Namo the constituents are formularies and rows: a **module** attaches (the operator form of `attach`, so `flows << OrderFlow` reads the same as `flows.attach(OrderFlow)`); a **Hash** appends as a data row; a **Row** (one drawn from another Namo) appends its underlying hash as a row. It returns the Namo, so the arms chain freely: `flows << OrderFlow << {date: 4, buys: 10, sells: 5} << Scoring`. Two things `<<` deliberately does not take: a bare callable (formula *registration* stays with `[]=`, which dispatches callable-vs-scalar and enforces data/formula exclusivity) and a whole Namo (combining collections is `+`'s job) — both raise. A bare Hash is therefore always a row, never a body of formulae. Appending a row whose keys collide with an existing formula name **raises** an `ArgumentError` naming the collision — a name is data or derived, never both. The guard is symmetric with `attach`'s: `[]=` names one column at the call site, so its scalar form evicts a same-named formula as your explicit intent, but a row is a bulk append you didn't name column-by-column, so the safe response is to refuse rather than let data and a formula of the same name disagree on access. Contract or rename first, then append.
755
755
 
756
756
  A module brands itself a formulary by including `Namo::Formulary`. The marker is mandatory — `attach` raises `ArgumentError` for an untagged module, so an ordinary module of helpers can't be mistaken for a body of derivations. Within a formulary, the public methods are the derivations and `private` methods are helpers: `net` above never appears in `derived_dimensions` and never resolves as a dimension. The separation needs no per-method declaration — public or private says it.
757
757
 
@@ -768,6 +768,10 @@ Because each method is copied into the store, the rules already governing `[]=`
768
768
  - **Materialise-and-drop on projection.** Naming a formulary method in a projection snapshots its values into a data column *and drops the formula*, exactly as for a `[]=` formula: `flows[:date, :signed_volume]` returns a Namo reporting `:signed_volume` as data, not derived, with all access paths agreeing on the stored snapshot.
769
769
  - **Carry-through.** Selection, the set and composition operators, and a projection that *omits* the name all keep the formula live, computing against the result's own rows — the carry-through is free, because the formulae are ordinary store entries.
770
770
 
771
+ `attach!` is the forceful sibling of `attach`. Where `attach` refuses a data collision, `attach!` **evicts** the colliding data columns first — deleting each one exactly as assigning a proc through `[]=` clears a same-named data column — then attaches. The bang is consent: `attach` won't destroy a column you never named, but `attach!` is you saying you meant to. The motivating case is refreshing a formulary over a *materialised snapshot* — once a derived dimension has been projected into a stored column (materialise-and-drop, above), re-attaching the formulary that defined it collides with the now-data column, and `attach!` re-attaches over it in one step where `attach` would require you to contract the column away first. `<<` stays the guarded form: a module through `<<` routes to `attach`, so the operator never evicts — only an explicit `attach!` call does.
772
+
773
+ Note that there is no counterpart in `[]=`'s scalar-over-formula eviction and is worth naming: a formulary method that reads a column its own name evicts will **recurse** on access. The eviction removes the datum, and exclusive storage leaves no shadowed value behind the formula, so `Row#[]` re-enters the formula rather than falling back to data — a `def close(row); row[:close] * ...; end` attached with `attach!` over a `:close` data column has no `:close` data left to read. `[]=` rarely bites this way because you write the proc at the call site, beside the name it replaces; a formulary is authored elsewhere, so a method reading its own evicted column is easy to attach without noticing. Self-reference is unguarded here as everywhere in the formula mechanism — keep a formulary's inputs and outputs under distinct names, or `attach` (not `attach!`) and contract deliberately.
774
+
771
775
  Two Namos that expose the same names — one via `[]=`, one via a formulary — are `===`; a Namo with a formulary attached is not `===` to a vanilla Namo of the same data dimensions.
772
776
 
773
777
  Formulary methods take `row` explicitly and index it (`row[:buys]`), the same as `[]=` formulae. Resolving bare names inside a method body (`buys` for `row[:buys]`) and memoising derived values on a frozen Namo arrive in a later release; until then a formulary method is a plain function of its `row` (and `namo`).
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.24.0'
5
+ VERSION = '0.25.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -89,7 +89,7 @@ class Namo
89
89
  end
90
90
 
91
91
  def attach(modul)
92
- collisions = modul.public_instance_methods(false) & data_dimensions
92
+ collisions = attach_collisions(modul)
93
93
  unless collisions.empty?
94
94
  raise ArgumentError, "formulary methods collide with data dimensions: #{collisions.inspect}"
95
95
  end
@@ -97,6 +97,12 @@ class Namo
97
97
  self
98
98
  end
99
99
 
100
+ def attach!(modul)
101
+ attach_collisions(modul).each{|name| @data.each{|row| row.delete(name)}}
102
+ @formulae.attach(modul)
103
+ self
104
+ end
105
+
100
106
  def <<(constituent)
101
107
  case constituent
102
108
  when Module then attach(constituent)
@@ -260,10 +266,18 @@ class Namo
260
266
  end
261
267
 
262
268
  def add_row(row)
269
+ collisions = row.keys & derived_dimensions
270
+ unless collisions.empty?
271
+ raise ArgumentError, "row keys collide with formulae: #{collisions.inspect}"
272
+ end
263
273
  @data << row
264
274
  self
265
275
  end
266
276
 
277
+ def attach_collisions(modul)
278
+ modul.public_instance_methods(false) & data_dimensions
279
+ end
280
+
267
281
  def attach_included_formularies
268
282
  self.class.ancestors.reverse.each do |modul|
269
283
  next if modul.is_a?(Class) || !modul.include?(Namo::Formulary)
data/test/namo_test.rb CHANGED
@@ -747,6 +747,58 @@ describe Namo do
747
747
  end
748
748
  end
749
749
 
750
+ describe "#attach!" do
751
+ it "attaches as attach does for a non-colliding module" do
752
+ namo = Namo.new(flow_data)
753
+ _(namo.attach!(delta)).must_be_same_as namo
754
+ _(namo.values(:signed_volume)).must_equal [20, -40, 0]
755
+ end
756
+
757
+ it "evicts a colliding data column and registers the formulary method in its place" do
758
+ widened = Namo.new(flow_data.map{|row| row.merge(signed_volume: 999)})
759
+ widened.attach!(delta)
760
+ _(widened.data_dimensions).wont_include :signed_volume
761
+ _(widened.derived_dimensions).must_include :signed_volume
762
+ _(widened.dimensions.count(:signed_volume)).must_equal 1
763
+ end
764
+
765
+ it "agrees across all access paths on the evicted-then-derived name" do
766
+ widened = Namo.new(flow_data.map{|row| row.merge(signed_volume: 999)})
767
+ widened.attach!(delta)
768
+ _(widened.values(:signed_volume)).must_equal [20, -40, 0]
769
+ _(widened.entries.first[:signed_volume]).must_equal 20
770
+ _(widened[signed_volume: 20].values(:date)).must_equal [1]
771
+ end
772
+
773
+ it "evicts every colliding column when several collide" do
774
+ clash = Module.new do
775
+ include Namo::Formulary
776
+ def buys(row); row[:date] * 2; end
777
+ def sells(row); row[:date] * 3; end
778
+ end
779
+ namo = Namo.new(flow_data)
780
+ namo.attach!(clash)
781
+ _(namo.data_dimensions).wont_include :buys
782
+ _(namo.data_dimensions).wont_include :sells
783
+ _(namo.derived_dimensions).must_include :buys
784
+ _(namo.derived_dimensions).must_include :sells
785
+ _(namo.values(:buys)).must_equal [2, 4, 6]
786
+ _(namo.values(:sells)).must_equal [3, 6, 9]
787
+ end
788
+
789
+ it "leaves the data untouched when the module does not collide" do
790
+ namo = Namo.new(flow_data)
791
+ namo.attach!(delta)
792
+ _(namo.values(:buys)).must_equal [60, 80, 75]
793
+ _(namo.values(:sells)).must_equal [40, 120, 75]
794
+ end
795
+
796
+ it "raises through attach (the guarded form) where attach! evicts" do
797
+ widened = Namo.new(flow_data.map{|row| row.merge(signed_volume: 999)})
798
+ _(proc{widened.attach(delta)}).must_raise ArgumentError
799
+ end
800
+ end
801
+
750
802
  describe "#<<" do
751
803
  it "attaches a formulary, the same as #attach" do
752
804
  namo = Namo.new(flow_data)
@@ -794,6 +846,18 @@ describe Namo do
794
846
  _(namo.values(:date)).must_equal [1, 2, 3, 4, 5]
795
847
  _(namo.values(:signed_volume)).must_equal [20, -40, 0, 5, 0]
796
848
  end
849
+
850
+ it "raises an ArgumentError when an appended Hash's keys collide with a formula" do
851
+ namo = Namo.new(flow_data).attach(delta)
852
+ error = _(proc{namo << {date: 4, buys: 10, sells: 5, signed_volume: 999}}).must_raise ArgumentError
853
+ _(error.message).must_match(/signed_volume/)
854
+ end
855
+
856
+ it "raises likewise when an appended Row's keys collide with a formula" do
857
+ namo = Namo.new(flow_data).attach(delta)
858
+ row = Namo.new([{date: 4, buys: 10, sells: 5, signed_volume: 999}]).entries.first
859
+ _(proc{namo << row}).must_raise ArgumentError
860
+ end
797
861
  end
798
862
 
799
863
  describe "#derived_dimensions" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: namo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran