namo 0.23.0 → 0.24.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: d07dec8cde3d61366ec26720be42067c0fa10deb6aa1fd37977bf69ba16aa802
4
- data.tar.gz: c355e980ff842ddc90d6a3873e98bb7185fb788f44f610c18ace363059136e6f
3
+ metadata.gz: 9e1ecaf29846a7876a59dae0d82c6c9d4e1fddd84b0c6a101941214ed6544517
4
+ data.tar.gz: 4245b13c3485a504ab5c365803de282cb508579b2ca9aec0799df0f1ce653d68
5
5
  SHA512:
6
- metadata.gz: 17ac2640764b18b17521af477f71b0e5419d24d68a35ce0a7fc79acd963f5e9210331b39ca04a242780037e1a39558bd8fd7079146331b13068f80f811da6fd9
7
- data.tar.gz: b4ad7c11375029c45e9b00f2769368c8b48082a965105c95a9b4d23c86a81a24af15107695535a08ea78fa548fc733c84a6ac0df13635a089c4da1297cee1ed2
6
+ metadata.gz: 41156dc96b98f59f8ff0dcf0d78f3b80ed895f402e11af9a53ba2e6cdb13d8bf39f1a38f48f2457bfe8bcf9082d146b73dafa4663e068816b2339d6888fd25af
7
+ data.tar.gz: e9c1f033e17fd2b10ffab984ea6349b7641de8abf0f04f23f85c893793242abd9e795847244855ad18b8c3f74b5876cce16dd212b2d1d2a6d327998657c9a4dc
data/CHANGELOG CHANGED
@@ -1,6 +1,19 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260629
5
+ 0.24.0: ~ Namo#<<, Namo::Collection#<< — widen from a formulary-only alias into a polymorphic "append a constituent" operator.
6
+
7
+ A base Namo's constituents are formularies and rows: << dispatches a Module to attach (0.23.0 behaviour, unchanged), a Hash to a data-row append, and a Row to a row append of its underlying hash. It does not take a bare callable (formula registration stays with []=) or a whole Namo (combining is +'s job) — both raise. Collection#<< is reconciled to the same verb over its own constituents: a Namo is a member, a Module attaches, and a loose Hash/Row raises (a Collection's rows are derived from its members, so a loose row has no durable home). This Hash/Row divergence — a base Namo appends the row, a Collection refuses it — is deliberate, not an oversight.
8
+
9
+ 1. ~ lib/namo.rb: ~ Namo#<< — replace the alias_method :<<, :attach with a dispatching << that routes Module→attach, Row→add_row(row.to_h), Hash→add_row(row), else→TypeError. attach stays Module-only and unchanged, so the 0.23.0 collision-guard and formulary semantics carry over verbatim and a Module's << still reaches it. + private add_row(row), appending to @data and returning self (so << chains). A whole Namo falls to the else and raises (it is not a Module/Row/Hash) — combining is +. Row-append is a data mutation that does not pass through []=, so it does not trigger formula-eviction; a row carrying a formula-named key surfaces live through the formula on access, per existing precedence (not guarded — the exclusivity invariant is []='s).
10
+ 2. ~ lib/Namo/Collection.rb: ~ Collection#<< — keep the splat + flatten (a member or an array of them), but dispatch each constituent: Namo→add_member, Module→attach (inherited from Namo — a Collection carries formulae over its detail view), Hash/Row→ArgumentError redirecting to member-add, else→TypeError. + private add_member(member), the prior reject-by-name + append body extracted verbatim. The @data = detail.data rebuild after the loop is preserved, so member-adds re-materialise detail as before and a Module-only << rebuilds harmlessly. Module routes to the inherited Namo#attach; member-add behaviour is byte-identical to 0.23.0 for the Namo/array cases.
11
+ 3. ~ test/namo_test.rb: ~ the formularies "#<<" describe gains the row arms — a Hash appends as a data row (and returns the Namo), a Row appends its underlying hash, a whole Namo raises TypeError (+'s job), a bare callable raises TypeError ([]='s job), and a mixed chain interleaves rows and a formulary. The two 0.23.0 Module-arm tests are unchanged.
12
+ 4. ~ test/Namo/Collection_test.rb: ~ the "#<<" describe gains a formulary attach whose row-scoped method resolves over the detail rows, a collection-scoped method (the namo_collection convention) reaching members, a loose Hash and a loose Row each raising ArgumentError with the member-redirect message, and a chain mixing member-adds with a formulary attach. The 0.23.0 member-add tests are unchanged.
13
+ 5. ~ README.md: ~ the Formularies << note — << now appends a constituent (Module, Hash, or Row) to a base Namo, chains, and raises on a bare callable or a whole Namo; the bare-Hash-is-always-a-row and row-append-does-not-evict notes added. ~ the Collection "<< and unnamed members" section — Collection#<< dispatches member/Module/raise, with the deliberate Hash/Row divergence spelled out. + a "Collection formulae" subsection — the namo_collection convention (an ordinary two-arity formulary method reaching a collection view in its body), no marker and no second store.
14
+ 6. ~ ROADMAP.md: + the 0.24.0 release entry; current-state bumped to 0.24.0; pivot/reshaping recorded as a decision (long-form aggregation is in-grain future work, widening-to-a-grid is presentational and belongs at an output edge, not the algebra), and collection formulae noted as needing no new mechanism.
15
+ 7. ~ Namo::VERSION: /0.23.0/0.24.0/
16
+
4
17
  20260627
5
18
  0.23.0: + Namo::Formulary — A formulary is a reusable body of derived dimensions a Namo instance may draw upon.
6
19
 
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
- `<<` is the operator form of `attach`, so `flows << OrderFlow` reads the same and chains: `flows << OrderFlow << Scoring`.
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.
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
 
@@ -1073,6 +1073,8 @@ gt << SubAssembly.new(name: :powertrain, data: [...]) # replaces the existing
1073
1073
  gt << [front_suspension, rear_suspension] # adds each
1074
1074
  ```
1075
1075
 
1076
+ On a Collection `<<` dispatches to the *constituent* appropriate to a collection: a **member** (a Namo, or an array of them) is added as above; a **module** attaches a formulary (a Collection is a Namo, so it carries formulae over its detail view — see below); a loose **Hash or Row raises** an `ArgumentError` that redirects to member-add. This is the one place `Namo#<<` and `Collection#<<` deliberately diverge: a base Namo appends a loose row, a Collection refuses it — a Collection's rows are *derived* from its members, so a loose row has no durable home (the next member-add rebuilds the data view and would erase it). The honest response is to refuse and point at member-add. A whole Namo arriving at a Collection is unambiguously a member, never a row-merge, because `<<` has no whole-Namo arm (that is `+`).
1077
+
1076
1078
  There is no insertion-time guard against unnamed members. An unnamed member is simply appended (no name to collide on) and is unfindable by `find` — the honest consequence of having no name, not an error. `find(name)` returns the member with that name, or `nil`:
1077
1079
 
1078
1080
  ```ruby
@@ -1082,6 +1084,23 @@ gt.find(:missing) # => nil
1082
1084
 
1083
1085
  (`find(name)` is member lookup; it shadows `Enumerable#find` on Collections. Predicate search over rows remains available as `detect`.)
1084
1086
 
1087
+ #### Collection formulae
1088
+
1089
+ A formulary attached to a Collection resolves over its detail rows like any other — a Collection is a Namo, and its data view is the materialised detail. A *collection formula* needs no new mechanism: it is an ordinary two-arity formulary method whose `namo` argument is the Collection, reaching for a collection view (`summary`, `detail`, `members`) in its body. It resolves through the same `derive` path as every other formula; member-awareness lives in the method body, not in any marker or second store. By convention, name that argument `namo_collection` to signal it expects a Collection — a plain Namo lacking `members`/`summary` will `NoMethodError`, which is honest self-enforcement, no guard needed.
1090
+
1091
+ ```ruby
1092
+ module FleetMetrics
1093
+ include Namo::Formulary
1094
+
1095
+ def member_count(row, namo_collection)
1096
+ namo_collection.members.size
1097
+ end
1098
+ end
1099
+
1100
+ gt << FleetMetrics
1101
+ gt.values(:member_count) # => one entry per detail row, each the member count
1102
+ ```
1103
+
1085
1104
  #### View lifetime and liveness
1086
1105
 
1087
1106
  Materialisation is pure-live: the Collection rebuilds its data view from the current members on every `<<`, with no memoisation. So a mutation is reflected immediately — add a member, then summarise or detail, and the new member is included.
@@ -5,10 +5,15 @@ class Namo
5
5
  class Collection < Namo
6
6
  attr_reader :members
7
7
 
8
- def <<(*members)
9
- members.flatten.each do |member|
10
- @members.reject!{|existing| existing.name == member.name} unless member.name.nil?
11
- @members << member
8
+ def <<(*constituents)
9
+ constituents.flatten.each do |constituent|
10
+ case constituent
11
+ when Namo then add_member(constituent)
12
+ when Module then attach(constituent)
13
+ when Hash, Row
14
+ raise ArgumentError, "a Collection's rows come from its members; add a member (a named Namo), not a loose row"
15
+ else raise TypeError, "can't append #{constituent.class} to a Collection"
16
+ end
12
17
  end
13
18
  @data = detail.data
14
19
  self
@@ -48,5 +53,10 @@ class Namo
48
53
  @members = []
49
54
  super
50
55
  end
56
+
57
+ def add_member(member)
58
+ @members.reject!{|existing| existing.name == member.name} unless member.name.nil?
59
+ @members << member
60
+ end
51
61
  end
52
62
  end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.23.0'
5
+ VERSION = '0.24.0'
6
6
  end
data/lib/namo.rb CHANGED
@@ -97,7 +97,14 @@ class Namo
97
97
  self
98
98
  end
99
99
 
100
- alias_method :<<, :attach
100
+ def <<(constituent)
101
+ case constituent
102
+ when Module then attach(constituent)
103
+ when Row then add_row(constituent.to_h)
104
+ when Hash then add_row(constituent)
105
+ else raise TypeError, "can't append #{constituent.class} to a Namo; expected a Module (formulary), a Hash, or a Row"
106
+ end
107
+ end
101
108
 
102
109
  def +(other)
103
110
  raise_unless_namo(other)
@@ -252,6 +259,11 @@ class Namo
252
259
  attach_included_formularies
253
260
  end
254
261
 
262
+ def add_row(row)
263
+ @data << row
264
+ self
265
+ end
266
+
255
267
  def attach_included_formularies
256
268
  self.class.ancestors.reverse.each do |modul|
257
269
  next if modul.is_a?(Class) || !modul.include?(Namo::Formulary)
@@ -45,6 +45,20 @@ describe Namo::Collection do
45
45
  Namo::Collection.new.tap{|c| c << [powertrain, chassis, body]}
46
46
  end
47
47
 
48
+ let(:component_pricing) do
49
+ Module.new do
50
+ include Namo::Formulary
51
+ def cost_per_kg(row); row[:cost] / row[:weight]; end
52
+ end
53
+ end
54
+
55
+ let(:fleet_metrics) do
56
+ Module.new do
57
+ include Namo::Formulary
58
+ def member_count(row, namo_collection); namo_collection.members.size; end
59
+ end
60
+ end
61
+
48
62
  describe "construction" do
49
63
  it "starts with empty members" do
50
64
  _(Namo::Collection.new.members).must_equal []
@@ -101,6 +115,34 @@ describe Namo::Collection do
101
115
  collection = Namo::Collection.new
102
116
  _(collection << powertrain).must_be_same_as collection
103
117
  end
118
+
119
+ it "attaches a formulary whose row-scoped method resolves over the detail rows" do
120
+ collection << component_pricing
121
+ _(collection.values(:cost_per_kg)).must_equal [250, 250, 200, 250]
122
+ end
123
+
124
+ it "attaches a formulary whose collection-scoped method reaches members" do
125
+ collection << fleet_metrics
126
+ _(collection.values(:member_count)).must_equal [3, 3, 3, 3]
127
+ end
128
+
129
+ it "raises an ArgumentError on a loose Hash, redirecting to member-add" do
130
+ error = _(proc{collection << {component: 'bolt', weight: 1, cost: 5}}).must_raise ArgumentError
131
+ _(error.message).must_match(/member/)
132
+ end
133
+
134
+ it "raises an ArgumentError on a loose Row, redirecting to member-add" do
135
+ row = Namo.new([{component: 'bolt', weight: 1, cost: 5}]).entries.first
136
+ error = _(proc{collection << row}).must_raise ArgumentError
137
+ _(error.message).must_match(/member/)
138
+ end
139
+
140
+ it "chains member-adds and a formulary attach" do
141
+ collection = Namo::Collection.new
142
+ collection << powertrain << chassis << component_pricing
143
+ _(collection.members.size).must_equal 2
144
+ _(collection.values(:cost_per_kg)).must_equal [250, 250, 200]
145
+ end
104
146
  end
105
147
 
106
148
  describe "#find" do
data/test/namo_test.rb CHANGED
@@ -758,6 +758,42 @@ describe Namo do
758
758
  namo = Namo.new(flow_data)
759
759
  _(namo << delta).must_be_same_as namo
760
760
  end
761
+
762
+ it "appends a Hash as a data row" do
763
+ namo = Namo.new(flow_data)
764
+ namo << {date: 4, buys: 10, sells: 5}
765
+ _(namo.values(:date)).must_equal [1, 2, 3, 4]
766
+ _(namo.values(:buys)).must_equal [60, 80, 75, 10]
767
+ end
768
+
769
+ it "returns the Namo when appending a row" do
770
+ namo = Namo.new(flow_data)
771
+ _(namo << {date: 4, buys: 10, sells: 5}).must_be_same_as namo
772
+ end
773
+
774
+ it "appends a Row's underlying hash as a data row" do
775
+ namo = Namo.new(flow_data)
776
+ row = Namo.new([{date: 4, buys: 10, sells: 5}]).entries.first
777
+ namo << row
778
+ _(namo.values(:date)).must_equal [1, 2, 3, 4]
779
+ end
780
+
781
+ it "raises a TypeError on a whole Namo, which is +'s job" do
782
+ namo = Namo.new(flow_data)
783
+ _(proc{namo << Namo.new(flow_data)}).must_raise TypeError
784
+ end
785
+
786
+ it "raises a TypeError on a bare callable, which is []='s job" do
787
+ namo = Namo.new(flow_data)
788
+ _(proc{namo << ->(row){row[:buys]}}).must_raise TypeError
789
+ end
790
+
791
+ it "chains mixed arms: rows and a formulary" do
792
+ namo = Namo.new(flow_data)
793
+ namo << {date: 4, buys: 10, sells: 5} << delta << {date: 5, buys: 0, sells: 0}
794
+ _(namo.values(:date)).must_equal [1, 2, 3, 4, 5]
795
+ _(namo.values(:signed_volume)).must_equal [20, -40, 0, 5, 0]
796
+ end
761
797
  end
762
798
 
763
799
  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.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran