portage-ucp-journal 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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +21 -0
- data/README.md +50 -0
- data/lib/portage/ucp/journal/file_store.rb +61 -0
- data/lib/portage/ucp/journal/purchase_journal.rb +86 -0
- data/lib/portage/ucp/journal/store.rb +30 -0
- data/lib/portage/ucp/journal/version.rb +7 -0
- data/lib/portage/ucp/journal.rb +16 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9eff21bdd3bd61e9e0d06d28fb1730bdb6341cf03b3655ba314c374a6f300a22
|
|
4
|
+
data.tar.gz: db616a0e139f3cf13ada29d12995f3d7905993ba33da43b79b29b560d174105b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b78d052fe12a74a7bd05f75a83604c0c50c4670188bfc2ad75fc94c7d3dde6916b34485eaf97e66c487d3ac03046cabe92e90f2a29757584a0237b18410789eb
|
|
7
|
+
data.tar.gz: 1abe91206ecf4972ab684c0579e80b8e3ae49ba726e5d9ab3986fa4e7660e0b2c845c287d67e19767125694e5f04596c9417130eb9d3dbfcb55c70107cc0176b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/);
|
|
4
|
+
this project is pre-1.0, so APIs may still shift between minor versions.
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-09-15
|
|
7
|
+
|
|
8
|
+
- Initial release: `Portage::Ucp::Journal::Store` (the injectable, append-only
|
|
9
|
+
persistence seam design-log §22 asks for), `FileStore` (its JSON-Lines
|
|
10
|
+
default), and `PurchaseJournal` (one entry per line item of a settled
|
|
11
|
+
`Order` — store origin, source, product id, quantity, amount in minor
|
|
12
|
+
units + currency, order id, idempotency key, timestamp).
|
|
13
|
+
- No runtime dependency on `portage-ucp` — wires into `Dispatcher` via its
|
|
14
|
+
new optional `journal:` argument from the consumer's own app.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Whitbread
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Portage::Ucp::Journal
|
|
2
|
+
|
|
3
|
+
A buyer-side, append-only purchase journal for `portage-ucp` — store origin,
|
|
4
|
+
source, product, amount (minor units + currency), order id, idempotency key,
|
|
5
|
+
and timestamp for every completed purchase — built on a small, injectable
|
|
6
|
+
`Store` interface in the same mold as core's `RateLimiter`/`Authenticator`
|
|
7
|
+
(design-log §22).
|
|
8
|
+
|
|
9
|
+
This gem has no runtime dependency on `portage-ucp` itself, and `portage-ucp`
|
|
10
|
+
has none on this gem — core's `Dispatcher` gained an optional `journal:`
|
|
11
|
+
argument (nil by default, a no-op when absent) rather than requiring this
|
|
12
|
+
gem. You opt in from your own app.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "portage/ucp/journal"
|
|
18
|
+
|
|
19
|
+
journal = Portage::Ucp::Journal::PurchaseJournal.new # FileStore, ~/.portage/journal.jsonl, by default
|
|
20
|
+
|
|
21
|
+
dispatcher = Portage::Ucp::Dispatcher.new(adapter: my_adapter, journal: journal)
|
|
22
|
+
# ... complete_checkout dispatches as normal; each settled checkout that
|
|
23
|
+
# produces an order confirmation now gets one journal entry per line item.
|
|
24
|
+
|
|
25
|
+
journal.all
|
|
26
|
+
# => [{"shop"=>"example.myshopify.com", "source"=>"adapter:shopify",
|
|
27
|
+
# "product_id"=>"sku_1", "quantity"=>2, "amount"=>2000, "currency"=>"USD",
|
|
28
|
+
# "order_id"=>"order_1", "idempotency_key"=>"idem_1",
|
|
29
|
+
# "recorded_at"=>"2026-09-15T12:00:00Z"}, ...]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Swapping the store
|
|
33
|
+
|
|
34
|
+
`PurchaseJournal.new(store: your_store)` accepts anything implementing
|
|
35
|
+
`Portage::Ucp::Journal::Store`'s two methods (`#append(record)`,
|
|
36
|
+
`#each_record(&block)`). `FileStore` is the shipped default; nothing else
|
|
37
|
+
ships today (see `docs/plans/storage-abstraction-journal.md`'s open
|
|
38
|
+
decisions) — write your own `Store` subclass for Redis, a real database,
|
|
39
|
+
or an in-memory double for tests.
|
|
40
|
+
|
|
41
|
+
## What this is not
|
|
42
|
+
|
|
43
|
+
- Not `Support::TransactionLog` (payment-dispatch bookkeeping — reserve/
|
|
44
|
+
commit, spend caps) or `Support::OrderLedger` (a settled-order snapshot
|
|
45
|
+
keyed for lookup). Both stay in core `portage-ucp` — as of design-log §33
|
|
46
|
+
they gained the same pluggable-`Store` seam this gem has, but the classes
|
|
47
|
+
themselves did not move.
|
|
48
|
+
- Not a console, a CLI history command, or a scheduler. This gem is the
|
|
49
|
+
write path and the shared `Store` seam those would read from and build
|
|
50
|
+
on, not the read/UI surface itself.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Ucp
|
|
6
|
+
module Journal
|
|
7
|
+
# Default Store: a JSON-Lines file, one record per line. Genuinely
|
|
8
|
+
# append-only — unlike core's TransactionLog/OrderLedger, #append never
|
|
9
|
+
# reads or rewrites the whole file, so a large journal stays cheap to
|
|
10
|
+
# write to and a crash mid-append can only ever lose the one
|
|
11
|
+
# in-flight line, never corrupt an earlier one.
|
|
12
|
+
#
|
|
13
|
+
# Same raising-write posture as TransactionLog/OrderLedger: a lost
|
|
14
|
+
# write here is a silent hole in the buyer's own purchase record, so
|
|
15
|
+
# there is no `rescue StandardError; nil` anywhere in this class.
|
|
16
|
+
class FileStore < Store
|
|
17
|
+
PATH = File.join(Dir.home, ".portage", "journal.jsonl").freeze
|
|
18
|
+
|
|
19
|
+
def initialize(path: PATH)
|
|
20
|
+
super()
|
|
21
|
+
@path = path
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def append(record)
|
|
25
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
26
|
+
File.open(@path, File::WRONLY | File::CREAT | File::APPEND, 0o600) do |file|
|
|
27
|
+
file.flock(File::LOCK_EX)
|
|
28
|
+
file.write("#{JSON.generate(record)}\n")
|
|
29
|
+
file.flush
|
|
30
|
+
end
|
|
31
|
+
record
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Skips a torn trailing line (a crash mid-write leaves at most one
|
|
35
|
+
# unparseable final line) rather than raising — every earlier,
|
|
36
|
+
# complete line is still good and must not be lost because the last
|
|
37
|
+
# one wasn't. A torn line in the *middle* of the file would indicate
|
|
38
|
+
# something worse than a crash mid-append (the file was hand-edited,
|
|
39
|
+
# or two writers bypassed the flock) and is left to surface as a
|
|
40
|
+
# JSON::ParserError rather than silently swallowed anywhere but the
|
|
41
|
+
# last line.
|
|
42
|
+
def each_record
|
|
43
|
+
return enum_for(:each_record) unless block_given?
|
|
44
|
+
|
|
45
|
+
return unless File.exist?(@path)
|
|
46
|
+
|
|
47
|
+
lines = File.open(@path, File::RDONLY) do |file|
|
|
48
|
+
file.flock(File::LOCK_SH)
|
|
49
|
+
file.readlines
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
lines.each_with_index do |line, index|
|
|
53
|
+
yield JSON.parse(line)
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
raise unless index == lines.length - 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Portage
|
|
4
|
+
module Ucp
|
|
5
|
+
module Journal
|
|
6
|
+
# Buyer-side, append-only record of "what did I buy, where, for how
|
|
7
|
+
# much" (design-log §22) — distinct from core's TransactionLog
|
|
8
|
+
# (payment-dispatch bookkeeping) and OrderLedger (a settled-order
|
|
9
|
+
# snapshot keyed for later lookup). Neither of those is shaped as a
|
|
10
|
+
# journal, and neither belongs to a consumer wanting one durable
|
|
11
|
+
# append-only trail across every completed purchase regardless of
|
|
12
|
+
# which adapter or platform made it.
|
|
13
|
+
#
|
|
14
|
+
# Takes only what's already in hand at the dispatcher's settle point —
|
|
15
|
+
# no re-fetch, no new schema. `checkout` is duck-typed against
|
|
16
|
+
# Portage::Ucp::Checkout (currency, line_items — each a LineItem-shaped
|
|
17
|
+
# object with #item.id, a bare integer #quantity, and #totals, an
|
|
18
|
+
# array of Total-shaped objects with #type/#amount — and #order, an
|
|
19
|
+
# OrderConfirmation-shaped object with #id), not against
|
|
20
|
+
# Portage::Ucp::Order: Checkout#order is only ever an
|
|
21
|
+
# order_confirmation stub (id/permalink_url/label per the UCP schema),
|
|
22
|
+
# never the full Order with its own line_items. This gem carries no
|
|
23
|
+
# runtime dependency on portage-ucp itself either way.
|
|
24
|
+
class PurchaseJournal
|
|
25
|
+
def initialize(store: FileStore.new, clock: -> { Time.now })
|
|
26
|
+
@store = store
|
|
27
|
+
@clock = clock
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# One journal entry per line item — a checkout with several distinct
|
|
31
|
+
# products is several purchases from the buyer's own point of view,
|
|
32
|
+
# not one.
|
|
33
|
+
#
|
|
34
|
+
# @param shop [String, nil] merchant/store identity, same value
|
|
35
|
+
# Dispatcher already threads into TransactionLog.
|
|
36
|
+
# @param source ["native_ucp", String] "native_ucp" for a
|
|
37
|
+
# direct/no-adapter dispatch, "adapter:<platform>" otherwise — see
|
|
38
|
+
# docs/plans/storage-abstraction-journal.md's open decision on the
|
|
39
|
+
# exact platform string.
|
|
40
|
+
# @param checkout [#currency, #line_items, #order] the settled
|
|
41
|
+
# Checkout — Dispatcher's `result` at the complete_checkout settle
|
|
42
|
+
# point, same object #order_ledger snapshots `#order` from.
|
|
43
|
+
# @param idempotency_key [String] joins this entry back to the
|
|
44
|
+
# TransactionLog/OrderLedger records from the same dispatch.
|
|
45
|
+
# @return [Array<Hash>] the entries written, one per line item.
|
|
46
|
+
def record_checkout(shop:, source:, checkout:, idempotency_key:)
|
|
47
|
+
context = { shop: shop, source: source, order_id: checkout.order&.id,
|
|
48
|
+
idempotency_key: idempotency_key, recorded_at: @clock.call.utc.iso8601 }
|
|
49
|
+
|
|
50
|
+
checkout.line_items.map do |line_item|
|
|
51
|
+
entry = build_entry(line_item, checkout.currency, context)
|
|
52
|
+
@store.append(entry)
|
|
53
|
+
entry
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def each_record(&)
|
|
58
|
+
@store.each_record(&)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def all
|
|
62
|
+
enum_for(:each_record).to_a
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_entry(line_item, currency, context)
|
|
68
|
+
{
|
|
69
|
+
"shop" => context[:shop], "source" => context[:source], "product_id" => line_item.item.id,
|
|
70
|
+
"quantity" => line_item.quantity, "amount" => line_total(line_item), "currency" => currency,
|
|
71
|
+
"order_id" => context[:order_id], "idempotency_key" => context[:idempotency_key],
|
|
72
|
+
"recorded_at" => context[:recorded_at]
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Total#amount is a bare integer minor-unit amount — same convention
|
|
77
|
+
# Dispatcher#settled_amount already follows for the checkout-level
|
|
78
|
+
# total.
|
|
79
|
+
def line_total(line_item)
|
|
80
|
+
total = Array(line_item.totals).find { |t| t.type == "total" }
|
|
81
|
+
total&.amount
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Ucp
|
|
3
|
+
module Journal
|
|
4
|
+
# @abstract Pluggable append-only record store — same "no bundled
|
|
5
|
+
# storage assumption" posture as core's RateLimiter/Authenticator
|
|
6
|
+
# (portage-ucp §9), extended here to persistence (design-log §22).
|
|
7
|
+
# Unlike NullRateLimiter/UnconfiguredAuthenticator, there is no
|
|
8
|
+
# silent-no-op default: an unconfigured journal that drops every
|
|
9
|
+
# write defeats the point of a purchase record, so FileStore (this
|
|
10
|
+
# gem's own default) is a real, durable implementation, not a null
|
|
11
|
+
# object. The interface is deliberately minimal — append and replay
|
|
12
|
+
# only — because the only consumer today (PurchaseJournal) needs
|
|
13
|
+
# nothing more; extend it when a second real consumer (a console, a
|
|
14
|
+
# scheduler) needs keyed lookups, not speculatively ahead of one.
|
|
15
|
+
class Store
|
|
16
|
+
# @param record [Hash] a single journal entry, already built by the
|
|
17
|
+
# caller (PurchaseJournal). The store persists it as-is.
|
|
18
|
+
def append(record)
|
|
19
|
+
raise NotImplementedError, "#{self.class} must implement #append"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Yields each previously-appended record, in write order, without a
|
|
23
|
+
# block returns an Enumerator.
|
|
24
|
+
def each_record(&)
|
|
25
|
+
raise NotImplementedError, "#{self.class} must implement #each_record"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require_relative "journal/version"
|
|
2
|
+
require_relative "journal/store"
|
|
3
|
+
require_relative "journal/file_store"
|
|
4
|
+
require_relative "journal/purchase_journal"
|
|
5
|
+
|
|
6
|
+
module Portage
|
|
7
|
+
module Ucp
|
|
8
|
+
# Buyer-side purchase journal + the injectable Store abstraction it's
|
|
9
|
+
# built on (design-log §22) — kept out of the dependency-light core gem
|
|
10
|
+
# (portage-ucp §2) since it's an optional consumer add-on, not a
|
|
11
|
+
# payment-safety-critical concern the way TransactionLog/OrderLedger
|
|
12
|
+
# are. A future console or scheduler gem depends on Store the same way.
|
|
13
|
+
module Journal
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: portage-ucp-journal
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tom Whitbread
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: portage-ucp
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.6'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.13'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rubocop
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.88'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.88'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: yard
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.9'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.9'
|
|
69
|
+
description: 'An append-only, consumer-swappable record of every purchase a Dispatcher
|
|
70
|
+
completes (store origin, source, product, amount in minor units, order id, idempotency
|
|
71
|
+
key), built on a small Store interface in the rate_limiter/authenticator mold —
|
|
72
|
+
the shared persistence seam design-log §22 asks for so a future console or scheduler
|
|
73
|
+
gem doesn''t each grow an incompatible one. Zero runtime dependency on portage-ucp
|
|
74
|
+
itself; wires in via Dispatcher''s optional journal: argument.'
|
|
75
|
+
email:
|
|
76
|
+
executables: []
|
|
77
|
+
extensions: []
|
|
78
|
+
extra_rdoc_files: []
|
|
79
|
+
files:
|
|
80
|
+
- CHANGELOG.md
|
|
81
|
+
- LICENSE
|
|
82
|
+
- README.md
|
|
83
|
+
- lib/portage/ucp/journal.rb
|
|
84
|
+
- lib/portage/ucp/journal/file_store.rb
|
|
85
|
+
- lib/portage/ucp/journal/purchase_journal.rb
|
|
86
|
+
- lib/portage/ucp/journal/store.rb
|
|
87
|
+
- lib/portage/ucp/journal/version.rb
|
|
88
|
+
homepage: https://github.com/tomtom87/Portage/tree/main/portage-ucp-journal
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
source_code_uri: https://github.com/tomtom87/Portage/tree/main/portage-ucp-journal
|
|
93
|
+
changelog_uri: https://github.com/tomtom87/Portage/blob/main/portage-ucp-journal/CHANGELOG.md
|
|
94
|
+
rubygems_mfa_required: 'true'
|
|
95
|
+
post_install_message:
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.2'
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 3.5.22
|
|
111
|
+
signing_key:
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: Buyer-side purchase journal + injectable Store abstraction for portage-ucp
|
|
114
|
+
test_files: []
|