custodian-core 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f7964e4623e30dd16ef4b7c9dd708f49d143ecfd98146470b7a78eb582dcebea
4
+ data.tar.gz: e068635b45a959416e80289cc36bb6fe2e3cb3a93fa89b81ce48a81c63c38b7a
5
+ SHA512:
6
+ metadata.gz: a49ad921937a9c02817bea8da23c8761daa3c3a5af18dddb017d8853cf7fe01b7bf03b6a469e6a1c263f73043fd47af55dcda1dd509776f6a8d464bb460e4179
7
+ data.tar.gz: 96b332f0053a92e41c503ef9e2417228ead9dd52aeb8f1521a51c8e4c7c5c0c69b239b9eccd7002d154cf671807b14f3d53c5140778cde9e7902ce302bbad8bc
data/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ ## [Unreleased]
2
+
3
+ - Require Ruby 3.2+, support Rails 7.x/8.x and ancestry 4.x/5.x explicitly.
4
+ - Reject negative demands and non-finite, negative, or over-remaining numeric
5
+ action outcomes.
6
+ - Validate resolution strictness, snapshot phases safely across threads, and
7
+ traverse deep trees iteratively.
8
+ - Limit the built gem to consumer-facing code, migrations, signatures, and
9
+ top-level release documentation.
10
+
11
+ ## [0.1.0] - 2026-07-07
12
+
13
+ Initial release: a domain-agnostic chain-of-custody engine over a tree of
14
+ responsibilities, ready for satellite gems to build concrete domains on top
15
+ of. 107 examples, 0 RuboCop offenses.
16
+
17
+ ### Models
18
+
19
+ - `Node` — the ward: a tree (via `ancestry`) of binary or numeric demands,
20
+ with a polymorphic `subject` (which may be another `Node`).
21
+ - `Graph` — a container anchored at a single root `Node`, with a
22
+ polymorphic, optional `owner`.
23
+ - `Custody` — the agreement between a custodian and a ward: action name,
24
+ priority, JSON action params, and a validity/status model
25
+ (`eternal`/`fixed_term`/`punctual` × `active`/`at_risk`/`broken`/`expired`)
26
+ exposed via `currently_valid?`, `trustworthy?`, and `healthcheck`.
27
+ - `CustodyNodeRule` / `CustodyRepudiatedNode` — per-descendant exceptions
28
+ (`exclude`/`full`/`limit_pct`/`limit_amount` rules, and outright
29
+ repudiations) that qualify how a `Custody` applies across its ward's
30
+ subtree, surfaced through `Custody#applies_to?`.
31
+
32
+ ### ActionRegistry
33
+
34
+ - The gem's one extension point: `register`/`unregister`/`call`/
35
+ `registered?`/`clear!`, Symbol-normalized, Mutex-guarded.
36
+ - Strict outcome contract: `:resolved`, `:failed`, or a non-negative
37
+ `Numeric`, in the same unit as the node's `demand_value`. Anything else —
38
+ including negative Numerics — raises immediately rather than propagating
39
+ a silent bug downstream.
40
+
41
+ ### Adjuster
42
+
43
+ - `aggregate_demand` — pure, read-only consolidation of numeric demand
44
+ across a subtree in a single query, with binary nodes acting as a
45
+ magnitude firewall (they contribute no quantity to an ancestor's total,
46
+ but are still counted via `unresolved_binary_count`).
47
+ - `resolve_tree` — walks a subtree post-order, consulting each node's
48
+ direct custodies (filtered by caller-chosen `strictness: :valid` or
49
+ `:trustworthy`, and by `Custody#applies_to?`), then registered phases,
50
+ then escalating whatever remains to the direct parent with per-level
51
+ encapsulation: a child's shortfall becomes part of its parent's own
52
+ demand, with no marker distinguishing the two at invocation time. A
53
+ grandparent never sees a grandchild; the full story is only
54
+ reconstructable from each node's `attempts` audit trail. Binary
55
+ escalation is supported too: a parent's custodians can be asked to act on
56
+ a still-pending binary descendant's behalf.
57
+ - `register_phase` / `clear_phases!` — the hook a future sibling-generosity
58
+ satellite will plug into. Not built yet; only the extension point is.
59
+ - Pure resolution throughout: neither method persists or mutates any
60
+ record.
61
+
62
+ ### Documentation
63
+
64
+ - README with the project's guiding definition, vocabulary, quick start,
65
+ outcome contract, resolution order, and explicitly non-domain examples.
66
+ - Eight ADRs recording the project's design decisions, from the
67
+ domain-agnostic-core constraint through registry defensiveness.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 lucasnshuntervoa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # custodian-core
2
+
3
+ The core has no business rules from any specific domain — it only knows how
4
+ to resolve a chain of custody: identify the next responsible party, and
5
+ demand from them the execution of the action the previous ward could not
6
+ fulfill, climbing the tree until someone has the capacity to act.
7
+
8
+ `custodian-core` is a Ruby (Rails engine) gem that implements this
9
+ resolution mechanism over a tree of `Node` records. It ships no domain
10
+ vocabulary — no money, no storage, no networking — only the shape of
11
+ responsibility and the machinery to walk it. Concrete domains plug in as
12
+ satellite gems.
13
+
14
+ > ⚠️ **Early-stage (0.1.0).** The API may still change and this gem has not
15
+ > yet been battle-tested in production. It is provided "as is", without
16
+ > warranty of any kind (see [License](#license)). Use at your own risk.
17
+
18
+ ## Vocabulary
19
+
20
+ - **Node** — something that needs to be resolved: the **ward**. Nodes form
21
+ a tree (via [ancestry](https://github.com/stefankroes/ancestry)); a node's
22
+ demand is either binary ("this must be done") or numeric (a quantity to
23
+ cover).
24
+ - **Custody** — the agreement between a **custodian** (whoever takes on
25
+ responsibility for a ward) and the ward itself: which action to call, at
26
+ what priority, and under what validity window.
27
+ - **custodian** — the party who took on a `Custody`. It can be anything
28
+ polymorphic — including another `Node`.
29
+ - **Adjuster** — walks the tree and resolves it, invoking custodies through
30
+ the `ActionRegistry` and escalating what nobody could cover. Named after
31
+ the insurance claims adjuster: someone who reviews a policy, evaluates
32
+ what happened, and decides who's responsible for covering it.
33
+ - **ActionRegistry** — the plug-in point. Host apps and satellite gems
34
+ register named actions here; the core never knows what an action
35
+ actually does.
36
+
37
+ ## Quick start
38
+
39
+ ```ruby
40
+ root = Custodian::Core::Node.create!(demand_type: "fixed", demand_value: 100)
41
+ child = Custodian::Core::Node.create!(demand_type: "fixed", demand_value: 50, parent: root)
42
+
43
+ Custodian::Core::Custody.create!(ward: root, action_name: "cover_shortfall")
44
+
45
+ Custodian::Core::ActionRegistry.register(:cover_shortfall) do |node, custody, remaining|
46
+ remaining # fully resolves whatever it's asked to cover
47
+ end
48
+
49
+ result = Custodian::Core::Adjuster.resolve_tree(root)
50
+
51
+ result[child.id][:gap] #=> 0.0 (BigDecimal) - resolved via escalation into root
52
+ result[root.id][:gap] #=> 0.0 - root's own 100 + child's inherited 50 = 150, fully covered
53
+ result[root.id][:attempts]
54
+ #=> [{ custody_id: 1, action_name: "cover_shortfall", outcome: 0.15e3, via: :direct }]
55
+ ```
56
+
57
+ `resolve_tree` returns a Hash keyed by node id. Each entry reports what was
58
+ `demanded`, how much of it is `own_demand` vs `inherited_shortfall`, the
59
+ `resolved_amount` and remaining `gap`, binary-specific fields, and a full
60
+ `attempts` audit trail (`via: :direct | :phase | :escalation`) for
61
+ reconstructing the whole story after the fact.
62
+
63
+ ## The outcome contract
64
+
65
+ A registered action receives `(node, custody, remaining)` and must return:
66
+
67
+ - `:resolved` — fully satisfied, stop trying custodies for this node
68
+ - `:failed` — this custodian couldn't act, try the next one
69
+ - a non-negative `Numeric` — partial resolution, **in the same unit as the
70
+ node's `demand_value`** (see
71
+ [ADR 0004](docs/adr/0004-numeric-outcome-unit-commensurability.md); the
72
+ core cannot enforce unit correctness, only that the number itself is
73
+ well-formed)
74
+
75
+ Anything else — `true`, `nil`, a `String`, a negative number — raises
76
+ `ActionRegistry::InvalidOutcomeError` immediately. See
77
+ [ADR 0008](docs/adr/0008-registry-defensiveness.md) for why this is
78
+ deliberately strict rather than forgiving.
79
+
80
+ ## Security: action registration is a trust boundary
81
+
82
+ Registered actions and phase handlers are ordinary Ruby blocks that
83
+ `custodian-core` executes with your application's full privileges. The core
84
+ validates only the *shape* of what an action returns — it does **not**
85
+ sandbox what an action does. Treat registration as privileged
86
+ configuration:
87
+
88
+ - Register actions only from code you control (your app, or satellite gems
89
+ you trust). Never build or register an action from untrusted input.
90
+ - Never derive an `action_name` to invoke, or an action's body, from
91
+ user-supplied data without validating it against a fixed allowlist of
92
+ known actions.
93
+ - `action_params` on a `Custody` is passed to the action as-is; validate
94
+ and authorize any host-exposed path that lets an end user set
95
+ `action_name`, `action_params`, or custody attributes, since those
96
+ determine what code runs and on whose behalf.
97
+
98
+ ## Resolution order and encapsulation
99
+
100
+ For each node, post-order (children before parents), the Adjuster tries, in
101
+ order: **direct custodies** (by `priority_weight`, then id), then
102
+ **registered phases** (an extension point, e.g. for a future
103
+ sibling-generosity satellite), then **escalation**.
104
+
105
+ Escalation is the gem's most distinctive idea: if a node still can't cover
106
+ its demand after the two steps above, that shortfall is folded into its
107
+ direct parent's own demand before the parent is even processed — with no
108
+ marker distinguishing "my own demand" from "what my child couldn't cover."
109
+ The parent's custodians never know a child failed; they just see a bigger
110
+ number. This means **a grandparent never sees a grandchild** — each level
111
+ only ever perceives one level down, exactly like a real management chain.
112
+ The full story (who ultimately paid for whom) is only reconstructable from
113
+ the `attempts` audit trail, not from any single node's own view. See
114
+ [ADR 0006](docs/adr/0006-escalation-with-per-level-encapsulation.md).
115
+
116
+ Binary nodes are a **magnitude firewall**: a numeric node's demand never
117
+ aggregates through a binary ancestor, even though escalation still walks
118
+ *into* one if it's the direct parent — aggregation and escalation are
119
+ different mechanisms. See
120
+ [ADR 0005](docs/adr/0005-binary-nodes-as-magnitude-firewalls.md).
121
+
122
+ ## Strictness: `:valid` vs `:trustworthy`
123
+
124
+ `resolve_tree(root, strictness: :valid)` (the default) consults any custody
125
+ that is currently legally valid, regardless of risk signals.
126
+ `strictness: :trustworthy` additionally excludes custodies flagged
127
+ `at_risk` — for callers who care not just "is this custody in force" but
128
+ "do I actually trust it right now." See
129
+ [ADR 0007](docs/adr/0007-strictness-as-a-caller-choice.md).
130
+
131
+ ## This is domain-agnostic — satellite gems bring the meaning
132
+
133
+ `custodian-core` never mentions money, storage, or networking. Examples of
134
+ what a satellite gem could layer on top, none of which live here:
135
+
136
+ - **Finance**: a `custodian-payments` gem registering `:charge_card` or
137
+ `:draw_from_reserve` actions, where a Node's numeric demand is an amount
138
+ owed and escalation models who is ultimately liable for a shortfall.
139
+ - **Storage failover**: a `custodian-storage` gem registering
140
+ `:replicate_to_backup`, where a binary Node represents "this object must
141
+ be durably stored somewhere" and custody escalates through a chain of
142
+ storage backends.
143
+ - **Network routing**: a `custodian-routing` gem registering
144
+ `:forward_via_peer`, where demand is bandwidth to place and unresolved
145
+ capacity escalates to an upstream peer.
146
+
147
+ ## Satellites (not yet built)
148
+
149
+ Planned extensions that do not exist in this gem yet:
150
+
151
+ - **Sibling generosity / "brotherhood"** — a phase (via `register_phase`)
152
+ that lets a resolved sibling's surplus cover another sibling's shortfall
153
+ before escalating to the parent.
154
+ - **Resource pools** — shared capacity a custodian can draw from across
155
+ multiple wards, instead of resolving each in isolation.
156
+ - **Execution history / honor tracking** — a record of how reliably a
157
+ custodian has actually resolved what it took on, feeding back into future
158
+ eligibility decisions.
159
+
160
+ ## Installation
161
+
162
+ ### Compatibility
163
+
164
+ `custodian-core` 0.1 supports Ruby 3.2 or newer, Rails 7.x and 8.x, and
165
+ `ancestry` 4.x and 5.x. The lower Ruby bound keeps the supported runtime
166
+ aligned with the CI matrix and with the currently supported Rails majors.
167
+
168
+ This gem is not published to RubyGems yet. While it lives on GitHub only,
169
+ add it to your host Rails application's Gemfile via git:
170
+
171
+ ```ruby
172
+ gem "custodian-core", git: "https://github.com/lucasnssoftwareengineer-alt/custodian-core"
173
+ ```
174
+
175
+ Once it is published to RubyGems, this becomes simply:
176
+
177
+ ```ruby
178
+ gem "custodian-core"
179
+ ```
180
+
181
+ Then install the engine's migrations:
182
+
183
+ ```sh
184
+ bundle exec rails custodian_core:install:migrations
185
+ bundle exec rails db:migrate
186
+ ```
187
+
188
+ ## Running tests
189
+
190
+ ```sh
191
+ bundle exec rake # runs both rspec and rubocop
192
+ bundle exec rspec # specs only
193
+ bundle exec rubocop # lint only
194
+ ```
195
+
196
+ ## Architecture decisions
197
+
198
+ See [docs/adr](docs/adr/0000-index.md) for the full record of design
199
+ decisions and their rationale.
200
+
201
+ ## License
202
+
203
+ The gem is available as open source under the terms of the
204
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Custodian
4
+ module Core
5
+ class ApplicationRecord < ActiveRecord::Base
6
+ self.abstract_class = true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Custodian
4
+ module Core
5
+ class Custody < ApplicationRecord
6
+ self.table_name = "custodian_core_custodies"
7
+
8
+ VALIDITY_TYPES = %w[eternal fixed_term punctual].freeze
9
+ STATUSES = %w[active at_risk broken expired].freeze
10
+
11
+ belongs_to :custodian, polymorphic: true, optional: true
12
+ belongs_to :ward, class_name: "Custodian::Core::Node"
13
+
14
+ has_many :node_rules, class_name: "Custodian::Core::CustodyNodeRule", dependent: :destroy
15
+ has_many :repudiated_nodes, class_name: "Custodian::Core::CustodyRepudiatedNode", dependent: :destroy
16
+
17
+ validates :action_name, presence: true
18
+ validates :priority_weight, numericality: { only_integer: true }
19
+ validates :validity_type, inclusion: { in: VALIDITY_TYPES }
20
+ validates :status, inclusion: { in: STATUSES }
21
+
22
+ # Answers only the exclusion question: is this node repudiated, or does
23
+ # it have an "exclude" rule? It does NOT interpret limit_pct/limit_amount
24
+ # rule values (i.e. it doesn't decide how much of the demand applies) -
25
+ # that quantitative interpretation is the Adjuster's job, in a later step.
26
+ def applies_to?(node)
27
+ return false if repudiated_nodes.exists?(node: node)
28
+ return false if node_rules.exists?(node: node, rule_type: "exclude")
29
+
30
+ true
31
+ end
32
+
33
+ def at_risk?
34
+ status == "at_risk"
35
+ end
36
+
37
+ def trustworthy?(at_time = Time.current)
38
+ currently_valid?(at_time) && !at_risk?
39
+ end
40
+
41
+ def healthcheck(at_time = Time.current)
42
+ return :broken if status == "broken"
43
+ return :expired if status == "expired"
44
+ return :expired unless currently_valid?(at_time)
45
+ return :at_risk if at_risk?
46
+
47
+ :active
48
+ end
49
+
50
+ def currently_valid?(at_time = Time.current)
51
+ return false if %w[broken expired].include?(status)
52
+
53
+ case validity_type
54
+ when "eternal"
55
+ true
56
+ when "fixed_term"
57
+ fixed_term_valid_at?(at_time)
58
+ when "punctual"
59
+ punctual_valid_at?(at_time)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def fixed_term_valid_at?(at_time)
66
+ (valid_from.nil? || at_time >= valid_from) && (valid_until.nil? || at_time <= valid_until)
67
+ end
68
+
69
+ def punctual_valid_at?(at_time)
70
+ valid_until.nil? || at_time <= valid_until
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Custodian
4
+ module Core
5
+ class CustodyNodeRule < ApplicationRecord
6
+ self.table_name = "custodian_core_custody_node_rules"
7
+
8
+ RULE_TYPES = %w[exclude full limit_pct limit_amount].freeze
9
+
10
+ belongs_to :custody, class_name: "Custodian::Core::Custody"
11
+ belongs_to :node, class_name: "Custodian::Core::Node"
12
+
13
+ validates :rule_type, inclusion: { in: RULE_TYPES }
14
+ validates :custody_id, uniqueness: { scope: :node_id }
15
+ validate :rule_value_matches_rule_type
16
+ validate :rule_value_within_percentage_bounds
17
+
18
+ scope :for_node, ->(node) { where(node: node) }
19
+
20
+ private
21
+
22
+ def rule_value_matches_rule_type
23
+ if %w[exclude full].include?(rule_type)
24
+ errors.add(:rule_value, "must be blank when rule_type is #{rule_type}") if rule_value.present?
25
+ elsif rule_value.nil?
26
+ errors.add(:rule_value, "can't be blank when rule_type is #{rule_type}")
27
+ end
28
+ end
29
+
30
+ def rule_value_within_percentage_bounds
31
+ return unless rule_type == "limit_pct" && rule_value.present?
32
+
33
+ return if rule_value.positive? && rule_value <= 100
34
+
35
+ errors.add(:rule_value, "must be greater than 0 and less than or equal to 100 for limit_pct")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Custodian
4
+ module Core
5
+ class CustodyRepudiatedNode < ApplicationRecord
6
+ self.table_name = "custodian_core_custody_repudiated_nodes"
7
+
8
+ belongs_to :custody, class_name: "Custodian::Core::Custody"
9
+ belongs_to :node, class_name: "Custodian::Core::Node"
10
+
11
+ validates :custody_id, uniqueness: { scope: :node_id }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Custodian
4
+ module Core
5
+ class Graph < ApplicationRecord
6
+ self.table_name = "custodian_core_graphs"
7
+
8
+ belongs_to :owner, polymorphic: true, optional: true
9
+ belongs_to :root_node, class_name: "Custodian::Core::Node"
10
+
11
+ validate :root_node_must_be_a_root
12
+
13
+ private
14
+
15
+ def root_node_must_be_a_root
16
+ return if root_node.nil? || root_node.root?
17
+
18
+ errors.add(:root_node, "must be a root node (cannot have a parent)")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ancestry"
4
+
5
+ module Custodian
6
+ module Core
7
+ class Node < ApplicationRecord
8
+ self.table_name = "custodian_core_nodes"
9
+
10
+ has_ancestry
11
+
12
+ belongs_to :subject, polymorphic: true, optional: true
13
+
14
+ DEMAND_TYPES = %w[binary fixed variable_manual variable_by_tag].freeze
15
+
16
+ validates :demand_type, presence: true, inclusion: { in: DEMAND_TYPES }
17
+ validates :demand_value, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
18
+ validate :demand_value_matches_demand_type
19
+
20
+ # Node deliberately has no graph_id column: graph membership is resolved
21
+ # via the ancestry root instead, to avoid an unused foreign key.
22
+ def graph
23
+ Graph.find_by(root_node: root)
24
+ end
25
+
26
+ private
27
+
28
+ def demand_value_matches_demand_type
29
+ if demand_type == "binary"
30
+ errors.add(:demand_value, "must be blank when demand_type is binary") if demand_value.present?
31
+ elsif demand_value.nil?
32
+ errors.add(:demand_value, "can't be blank unless demand_type is binary")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/custodian/core/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "custodian-core"
7
+ spec.version = Custodian::Core::VERSION
8
+ spec.authors = ["Lucas nunes de sousa"]
9
+ spec.email = ["lucas.ns.software.engineer@gmail.com"]
10
+ spec.homepage = "https://github.com/lucasnssoftwareengineer-alt/custodian-core"
11
+ spec.metadata["source_code_uri"] = "https://github.com/lucasnssoftwareengineer-alt/custodian-core"
12
+ spec.metadata["changelog_uri"] = "https://github.com/lucasnssoftwareengineer-alt/custodian-core/blob/main/CHANGELOG.md"
13
+ spec.summary = "A domain-agnostic chain-of-custody engine for Ruby."
14
+ spec.description = "Custodian::Core resolves a chain of custody over a tree of responsibilities: " \
15
+ "it identifies the next responsible party and demands the action a previous " \
16
+ "ward could not fulfill, climbing the tree until someone can act. It carries " \
17
+ "no business rules from any specific domain - concrete domains are implemented " \
18
+ "as satellite gems that register actions into this core via a registry."
19
+ spec.license = "MIT"
20
+ spec.required_ruby_version = ">= 3.2.0"
21
+
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ # Keep the package independent from Git and deliberately limited to files
25
+ # useful to consumers. Development configuration, specs, CI, and internal
26
+ # architecture records remain in the repository but not in the gem.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ %w[CHANGELOG.md LICENSE.txt README.md custodian-core.gemspec] +
29
+ Dir.glob(%w[app/**/*.rb db/**/*.rb lib/**/*.rb sig/**/*.rbs])
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ # This gem is a Rails engine so that satellite gems can rely on it to ship
36
+ # ActiveRecord models and migrations in later steps. We depend on the
37
+ # specific Rails components isolate_namespace/ActiveRecord actually need
38
+ # (actionpack for Rails::Engine's route set, activerecord for models)
39
+ # rather than the "rails" umbrella gem, to avoid pulling in ActionMailer,
40
+ # ActionCable, ActionText, ActiveStorage, ActionView, Nokogiri, etc. into a
41
+ # domain-agnostic core (see docs/adr/0001-domain-agnostic-core.md).
42
+ spec.add_dependency "actionpack", ">= 7.0", "< 9.0"
43
+ spec.add_dependency "activerecord", ">= 7.0", "< 9.0"
44
+ spec.add_dependency "activesupport", ">= 7.0", "< 9.0"
45
+ spec.add_dependency "ancestry", ">= 4.0", "< 6.0"
46
+ spec.add_dependency "railties", ">= 7.0", "< 9.0"
47
+
48
+ # For more information and examples about making a new gem, check out our
49
+ # guide at: https://bundler.io/guides/creating_gem.html
50
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCustodianCoreNodes < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :custodian_core_nodes do |t|
6
+ t.string :ancestry, index: true
7
+
8
+ t.string :subject_type
9
+ t.bigint :subject_id
10
+
11
+ t.string :demand_type, null: false
12
+ t.decimal :demand_value, precision: 15, scale: 4
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :custodian_core_nodes, %i[subject_type subject_id]
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCustodianCoreGraphs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :custodian_core_graphs do |t|
6
+ t.string :owner_type
7
+ t.bigint :owner_id
8
+
9
+ t.bigint :root_node_id, null: false
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :custodian_core_graphs, %i[owner_type owner_id]
15
+ add_foreign_key :custodian_core_graphs, :custodian_core_nodes, column: :root_node_id
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCustodianCoreCustodies < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :custodian_core_custodies do |t|
6
+ t.string :custodian_type
7
+ t.bigint :custodian_id
8
+
9
+ t.bigint :ward_id, null: false
10
+
11
+ t.string :action_name, null: false
12
+ t.json :action_params, null: false, default: {}
13
+
14
+ t.integer :priority_weight, null: false, default: 0
15
+
16
+ t.string :validity_type, null: false, default: "eternal"
17
+ t.datetime :valid_from
18
+ t.datetime :valid_until
19
+
20
+ t.string :status, null: false, default: "active"
21
+
22
+ t.timestamps
23
+ end
24
+
25
+ add_index :custodian_core_custodies, %i[custodian_type custodian_id],
26
+ name: "index_custodian_core_custodies_on_custodian"
27
+ add_foreign_key :custodian_core_custodies, :custodian_core_nodes, column: :ward_id
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCustodianCoreCustodyNodeRules < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :custodian_core_custody_node_rules do |t|
6
+ t.bigint :custody_id, null: false
7
+ t.bigint :node_id, null: false
8
+
9
+ t.string :rule_type, null: false
10
+ t.decimal :rule_value, precision: 15, scale: 4
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :custodian_core_custody_node_rules, %i[custody_id node_id],
16
+ unique: true, name: "index_custody_node_rules_on_custody_and_node"
17
+ add_foreign_key :custodian_core_custody_node_rules, :custodian_core_custodies, column: :custody_id
18
+ add_foreign_key :custodian_core_custody_node_rules, :custodian_core_nodes, column: :node_id
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCustodianCoreCustodyRepudiatedNodes < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :custodian_core_custody_repudiated_nodes do |t|
6
+ t.bigint :custody_id, null: false
7
+ t.bigint :node_id, null: false
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :custodian_core_custody_repudiated_nodes, %i[custody_id node_id],
13
+ unique: true, name: "index_custody_repudiated_nodes_on_custody_and_node"
14
+ add_foreign_key :custodian_core_custody_repudiated_nodes, :custodian_core_custodies, column: :custody_id
15
+ add_foreign_key :custodian_core_custody_repudiated_nodes, :custodian_core_nodes, column: :node_id
16
+ end
17
+ end