kettle-family 0.1.16 → 0.1.17

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: 927846e1574bcad8790c8cabb3a7c06cd9afbf8e0637354cd52bd8d5c08e959b
4
- data.tar.gz: 958338770647aabe0a916d81a9ec7ed76fac09feec4670d53a25c57120b1e59b
3
+ metadata.gz: eee55351e363790b515db4ac4534a1763ac7dbbabb772acebabaaafda8579e58
4
+ data.tar.gz: 20fb7c07c3c12f2591bda364291859fd08721f68f24c4f5c0f0bb4fa585a1cd5
5
5
  SHA512:
6
- metadata.gz: b750b13b10b5433d06d79a35a62bc7169e9321a3ca44b0af45bffb30b5fa713a12e4f026612c336a2bb761ef66f92abef17c66f35d0ebeceaf2ff3b75d9d8f6a
7
- data.tar.gz: f36e586239ca4a3560dbae7d79357ac0ee2d6482e40b375c9e8c262849424b97395a230afc27f97d2770788cc81bc2a69b9ae5142670162c4abc66caea7d9709
6
+ metadata.gz: e7b378f322009660bd631d6ba9768bbee53ed53cdd1e2fd572f40f41bf0ba93a05f0f14cbc3232d8c649fc959b5c4ff0da1f2323127cef2e642dc906bd922e9b
7
+ data.tar.gz: 6aa53a5bbc7dbeca03d986d9980a7908f9e0013cb35c5e5d5ee461ad6736a7f7b65a2a03d483cacfc377a51e67ab68af795cf5c0eb7af8957878397cde356c2b
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -20,6 +20,25 @@ Please file a bug if you notice a violation of semantic versioning.
20
20
 
21
21
  ### Added
22
22
 
23
+ ### Changed
24
+
25
+ ### Deprecated
26
+
27
+ ### Removed
28
+
29
+ ### Fixed
30
+
31
+ ### Security
32
+
33
+ ## [0.1.17] - 2026-06-25
34
+
35
+ - TAG: [v0.1.17][0.1.17t]
36
+ - COVERAGE: 94.67% -- 1510/1595 lines in 21 files
37
+ - BRANCH COVERAGE: 76.00% -- 532/700 branches in 21 files
38
+ - 39.63% documented
39
+
40
+ ### Added
41
+
23
42
  - `kettle-family push`, `kettle-family pull`, and `kettle-family up` now plan
24
43
  or execute family-wide git synchronization commands.
25
44
  - `kettle-family gha-sha-pins` now plans or executes
@@ -28,12 +47,8 @@ Please file a bug if you notice a violation of semantic versioning.
28
47
  - `kettle-family bump-version` now accepts the same relative bump targets as
29
48
  `kettle-bump` (`major`, `minor`, `patch`, and `pre`) and applies them per
30
49
  member from each member's current version.
31
-
32
- ### Changed
33
-
34
- ### Deprecated
35
-
36
- ### Removed
50
+ - Text output from `kettle-family` now starts with the loaded `kettle-family`
51
+ version so local runs show which executable is active.
37
52
 
38
53
  ### Fixed
39
54
 
@@ -48,8 +63,17 @@ Please file a bug if you notice a violation of semantic versioning.
48
63
  version bump edits, and uses member-local branch target stacks so branch
49
64
  traversal can continue safely.
50
65
  - Text reports now indent each line of multi-line command output consistently.
51
-
52
- ### Security
66
+ - `kettle-family release-state` now recovers member-local branch-stack release
67
+ configuration from another local branch when the current branch does not carry
68
+ `.kettle-family.yml`, restoring branch-matrix output for branch-stack families.
69
+ - Branch `release-state` rows now report the latest released version from that
70
+ branch's major line instead of the repository-wide latest tag.
71
+ - Member-local branch-stack configuration is now discovered through the same
72
+ shared path for `install`, `bump-version`, `add-changelog`, workflow commands,
73
+ and `release-state`, including configs that only exist on another local
74
+ branch.
75
+ - Branch lane audits now run as part of `kettle-family check`, and
76
+ `branch-lanes` is no longer advertised as a separate user-facing command.
53
77
 
54
78
  ## [0.1.11] - 2026-06-23
55
79
 
@@ -295,7 +319,9 @@ Please file a bug if you notice a violation of semantic versioning.
295
319
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
296
320
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
297
321
 
298
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.11...HEAD
322
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.17...HEAD
323
+ [0.1.17]: https://github.com/kettle-dev/kettle-family/compare/v0.1.11...v0.1.17
324
+ [0.1.17t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.17
299
325
  [0.1.12]: https://github.com/kettle-dev/kettle-family/compare/v0.1.11...v0.1.12
300
326
  [0.1.12t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.12
301
327
  [0.1.11]: https://github.com/kettle-dev/kettle-family/compare/v0.1.10...v0.1.11
data/README.md CHANGED
@@ -571,7 +571,7 @@ Thanks for RTFM. ☺️
571
571
  [📌gitmoji]: https://gitmoji.dev
572
572
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
573
573
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
574
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.420-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
574
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.595-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
575
575
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
576
576
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
577
577
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "yaml"
5
+
6
+ module Kettle
7
+ module Family
8
+ module BranchTargetConfig
9
+ MAIN_BRANCH_SKIPPING_COMMANDS = %w[install release].freeze
10
+
11
+ module_function
12
+
13
+ def branch_targets_for(command, branches)
14
+ return branches unless MAIN_BRANCH_SKIPPING_COMMANDS.include?(command)
15
+
16
+ branches.reject { |branch| branch == "main" }
17
+ end
18
+
19
+ def member_local_release_config(member:, config:)
20
+ member_config = Config.load(root: member.root)
21
+ member_config = member_local_release_config_from_branch(member) || member_config unless member_config.path
22
+ return unless member_config.path
23
+ return if same_config_path?(config&.path, member_config.path)
24
+ return if member_config.release_target_branches.empty?
25
+
26
+ member_config
27
+ rescue Errno::ENOENT
28
+ nil
29
+ end
30
+
31
+ def same_config_path?(left, right)
32
+ return false if left.to_s.empty? || right.to_s.empty?
33
+ return false unless File.file?(left) && File.file?(right)
34
+
35
+ File.realpath(left) == File.realpath(right)
36
+ end
37
+
38
+ def member_local_release_config_from_branch(member)
39
+ root = member_git_root(member)
40
+ relative_root = member_relative_root(member, root)
41
+ member_local_config_paths(root, relative_root).each do |config_ref, content|
42
+ loaded = YAML.safe_load(content) || {}
43
+ branch_config = Config.new(root: member.root, path: config_ref, data: loaded)
44
+ return branch_config unless branch_config.release_target_branches.empty?
45
+ end
46
+ nil
47
+ rescue Error, Psych::SyntaxError
48
+ nil
49
+ end
50
+
51
+ def member_git_root(member)
52
+ stdout, stderr, status = Open3.capture3("git", "rev-parse", "--show-toplevel", chdir: member.root)
53
+ raise Error, "could not determine git root for #{member.root}: #{stderr}" unless status.success?
54
+
55
+ File.realpath(stdout.strip)
56
+ end
57
+
58
+ def member_relative_root(member, root)
59
+ member_root = File.realpath(member.root)
60
+ return "." if member_root == root
61
+ return member_root.delete_prefix("#{root}/") if member_root.start_with?("#{root}/")
62
+
63
+ raise Error, "member root #{member.root} is outside git root #{root}"
64
+ end
65
+
66
+ def member_local_config_paths(root, relative_root)
67
+ branches = local_branches(root)
68
+ candidates = Config::DEFAULT_PATHS.map do |path|
69
+ (relative_root == ".") ? path : File.join(relative_root, path)
70
+ end
71
+ branches.each_with_object([]) do |branch, memo|
72
+ candidates.each do |path|
73
+ content = git_show(root, "#{branch}:#{path}")
74
+ memo << ["#{branch}:#{path}", content] if content
75
+ end
76
+ end
77
+ end
78
+
79
+ def local_branches(root)
80
+ stdout, stderr, status = Open3.capture3("git", "for-each-ref", "--format=%(refname:short)", "refs/heads", chdir: root)
81
+ raise Error, "could not list local branches for #{root}: #{stderr}" unless status.success?
82
+
83
+ stdout.lines.map(&:strip).reject(&:empty?)
84
+ end
85
+
86
+ def git_show(root, revision)
87
+ stdout, _stderr, status = Open3.capture3("git", "show", revision, chdir: root)
88
+ status.success? ? stdout : nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -8,7 +8,6 @@ module Kettle
8
8
  class CLI
9
9
  COMMANDS = %w[discover plan report metadata check test lint docs template gha-sha-pins install bump-version add-changelog release push pull up branch-lanes release-state].freeze
10
10
  WORKFLOW_COMMANDS = %w[check test lint docs template gha-sha-pins release push pull up].freeze
11
- MAIN_BRANCH_SKIPPING_COMMANDS = %w[install release].freeze
12
11
 
13
12
  def self.call(argv, out: $stdout, err: $stderr)
14
13
  new(argv, out: out, err: err).call
@@ -48,6 +47,8 @@ module Kettle
48
47
 
49
48
  def help
50
49
  out.puts(<<~HELP)
50
+ kettle-family: #{Kettle::Family::VERSION}
51
+
51
52
  Usage: kettle-family COMMAND [options]
52
53
  kettle-family bump-version VERSION|major|minor|patch|pre [options]
53
54
 
@@ -69,7 +70,6 @@ module Kettle
69
70
  push Plan or execute git push per member
70
71
  pull Plan or execute git pull --rebase per member
71
72
  up Plan or execute git pull --rebase then git push per member
72
- branch-lanes Audit configured branch lane release mappings
73
73
  release-state Report changelog release state for family members
74
74
 
75
75
  Options:
@@ -184,7 +184,7 @@ module Kettle
184
184
  selected_members: selected,
185
185
  config_path: config.path,
186
186
  branch_lanes: config.branch_lanes,
187
- release_target_branches: branch_targets_for(command, config.release_target_branches),
187
+ release_target_branches: BranchTargetConfig.branch_targets_for(command, config.release_target_branches),
188
188
  member_release_target_branches: member_release_target_branches(command: command, members: selected, config: config),
189
189
  release_mode: release_mode(command: command, options: options),
190
190
  command: command,
@@ -237,7 +237,7 @@ module Kettle
237
237
 
238
238
  def member_local_branch_target_command?(command, config, members)
239
239
  return false if !config.release_target_branches.empty?
240
- return false unless command == "bump-version"
240
+ return false unless %w[bump-version install add-changelog].include?(command)
241
241
 
242
242
  members.any? { |member| member_local_release_config(member: member, config: config) }
243
243
  end
@@ -245,7 +245,7 @@ module Kettle
245
245
  def branch_target_command_results(command:, config:, members:, options:)
246
246
  runner = CommandRunner.new(execute: options[:execute])
247
247
  selected_names = members.map(&:name)
248
- branch_targets_for(command, config.release_target_branches).each_with_object([]) do |branch, memo|
248
+ BranchTargetConfig.branch_targets_for(command, config.release_target_branches).each_with_object([]) do |branch, memo|
249
249
  memo << runner.call(
250
250
  member: family_member(config),
251
251
  phase: "release_checkout",
@@ -273,7 +273,7 @@ module Kettle
273
273
  next
274
274
  end
275
275
 
276
- branch_targets_for(command, member_config.release_target_branches).each do |branch|
276
+ BranchTargetConfig.branch_targets_for(command, member_config.release_target_branches).each do |branch|
277
277
  memo << runner.call(
278
278
  member: member,
279
279
  phase: "release_checkout",
@@ -411,25 +411,12 @@ module Kettle
411
411
  def member_release_target_branches(command:, members:, config:)
412
412
  members.each_with_object({}) do |member, memo|
413
413
  member_config = member_local_release_config(member: member, config: config)
414
- memo[member.name] = branch_targets_for(command, member_config.release_target_branches) if member_config
414
+ memo[member.name] = BranchTargetConfig.branch_targets_for(command, member_config.release_target_branches) if member_config
415
415
  end
416
416
  end
417
417
 
418
- def branch_targets_for(command, branches)
419
- return branches unless MAIN_BRANCH_SKIPPING_COMMANDS.include?(command)
420
-
421
- branches.reject { |branch| branch == "main" }
422
- end
423
-
424
418
  def member_local_release_config(member:, config:)
425
- member_config = Config.load(root: member.root)
426
- return unless member_config.path
427
- return if config.path && File.realpath(member_config.path) == File.realpath(config.path)
428
- return if member_config.release_target_branches.empty?
429
-
430
- member_config
431
- rescue Errno::ENOENT
432
- nil
419
+ BranchTargetConfig.member_local_release_config(member: member, config: config)
433
420
  end
434
421
 
435
422
  def install_order(members, config)
@@ -4,6 +4,7 @@ require "json"
4
4
  require "fileutils"
5
5
  require "open3"
6
6
  require "rbconfig"
7
+ require "rubygems"
7
8
  require "securerandom"
8
9
 
9
10
  module Kettle
@@ -57,6 +58,7 @@ module Kettle
57
58
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
58
59
  success = status.success?
59
60
  state = success ? JSON.parse(stdout) : {}
61
+ state = branch_filtered_state(member, state, branch) if success && branch
60
62
  result(member: member, command: command, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state, branch: branch)
61
63
  rescue JSON::ParserError => error
62
64
  result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
@@ -119,6 +121,43 @@ module Kettle
119
121
  config ? config.changelog_env : {}
120
122
  end
121
123
 
124
+ def branch_filtered_state(member, state, _branch)
125
+ latest_released = branch_latest_released(member, state["latest_changelog_version"])
126
+ return state unless latest_released
127
+
128
+ state.merge("latest_released" => latest_released)
129
+ rescue ArgumentError
130
+ state
131
+ end
132
+
133
+ def branch_latest_released(member, line_version)
134
+ target_major = gem_version(line_version).segments.first
135
+ versions = release_tag_versions(member.root).select do |version|
136
+ version.segments.first == target_major
137
+ end
138
+ versions.max&.to_s
139
+ end
140
+
141
+ def release_tag_versions(root)
142
+ stdout, stderr, status = Open3.capture3("git", "tag", "--list", "v*", chdir: root)
143
+ raise Error, "could not list release tags for #{root}: #{stderr}" unless status.success?
144
+
145
+ stdout.lines.filter_map do |line|
146
+ tag = line.strip
147
+ next unless tag.start_with?("v")
148
+
149
+ gem_version(tag.delete_prefix("v"))
150
+ rescue ArgumentError
151
+ nil
152
+ end
153
+ end
154
+
155
+ def gem_version(value)
156
+ raise ArgumentError, "missing version" if value.to_s.empty?
157
+
158
+ Gem::Version.new(value)
159
+ end
160
+
122
161
  def family_changelog_state(root)
123
162
  changelog = File.expand_path(config.changelog_path, root)
124
163
  raise Error, "missing root changelog #{config.changelog_path}" unless File.file?(changelog)
@@ -249,14 +288,7 @@ module Kettle
249
288
  end
250
289
 
251
290
  def member_local_release_config(member)
252
- member_config = Config.load(root: member.root)
253
- return unless member_config.path
254
- return if config&.path && File.realpath(member_config.path) == File.realpath(config.path)
255
- return if member_config.release_target_branches.empty?
256
-
257
- member_config
258
- rescue Errno::ENOENT
259
- nil
291
+ BranchTargetConfig.member_local_release_config(member: member, config: config)
260
292
  end
261
293
 
262
294
  def shared_changelog?
@@ -45,7 +45,7 @@ module Kettle
45
45
  end
46
46
 
47
47
  def to_text
48
- lines = ["family: #{family_name}"]
48
+ lines = ["kettle-family: #{Kettle::Family::VERSION}", "family: #{family_name}"]
49
49
  lines << "mode: #{family_mode}" if family_mode
50
50
  lines << "config: #{config_path || "none"}"
51
51
  lines << "order: #{order_mode}"
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "0.1.16"
6
+ VERSION = "0.1.17"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -20,7 +20,6 @@ module Kettle
20
20
  "pull" => [["pull", %w[git pull --rebase]]],
21
21
  "up" => [["pull", %w[git pull --rebase]], ["push", %w[git push]]]
22
22
  }.freeze
23
- MAIN_BRANCH_SKIPPING_COMMANDS = %w[release].freeze
24
23
 
25
24
  def initialize(command:, config:, members:, execute: false, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, local_ci: false, continue_ci_failures: false, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, gem_signing_password: nil)
26
25
  @command = command
@@ -80,7 +79,10 @@ module Kettle
80
79
  end
81
80
 
82
81
  def check_results(workflow_members)
83
- workflow_members.map { |member| ReadinessCheck.call(member: member, config: config) }
82
+ results = []
83
+ results.concat(BranchLaneAudit.new(config: config, members: workflow_members).results) unless config.branch_lanes.empty?
84
+ results.concat(workflow_members.map { |member| ReadinessCheck.call(member: member, config: config) })
85
+ results
84
86
  end
85
87
 
86
88
  def branch_target_results
@@ -215,20 +217,11 @@ module Kettle
215
217
  end
216
218
 
217
219
  def branch_targets
218
- return config.release_target_branches unless MAIN_BRANCH_SKIPPING_COMMANDS.include?(command)
219
-
220
- config.release_target_branches.reject { |branch| branch == "main" }
220
+ BranchTargetConfig.branch_targets_for(command, config.release_target_branches)
221
221
  end
222
222
 
223
223
  def member_local_release_config(member)
224
- member_config = Config.load(root: member.root)
225
- return unless member_config.path
226
- return if config.path && File.realpath(member_config.path) == File.realpath(config.path)
227
- return if member_config.release_target_branches.empty?
228
-
229
- member_config
230
- rescue Errno::ENOENT
231
- nil
224
+ BranchTargetConfig.member_local_release_config(member: member, config: config)
232
225
  end
233
226
 
234
227
  def checkout_branch_result(branch:, runner:)
data/lib/kettle/family.rb CHANGED
@@ -12,10 +12,11 @@ require_relative "family/changelog_check"
12
12
  require_relative "family/git_status"
13
13
  require_relative "family/version_bump"
14
14
  require_relative "family/branch_lane_audit"
15
+ require_relative "family/config"
16
+ require_relative "family/branch_target_config"
15
17
  require_relative "family/release_state_check"
16
18
  require_relative "family/local_install"
17
19
  require_relative "family/workflow"
18
- require_relative "family/config"
19
20
  require_relative "family/discovery"
20
21
  require_relative "family/orderer"
21
22
  require_relative "family/selection"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.16
4
+ version: 0.1.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -80,7 +80,7 @@ dependencies:
80
80
  version: '2.2'
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 2.2.15
83
+ version: 2.2.18
84
84
  type: :development
85
85
  prerelease: false
86
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  version: '2.2'
91
91
  - - ">="
92
92
  - !ruby/object:Gem::Version
93
- version: 2.2.15
93
+ version: 2.2.18
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: bundler-audit
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -168,7 +168,7 @@ dependencies:
168
168
  version: '2.0'
169
169
  - - ">="
170
170
  - !ruby/object:Gem::Version
171
- version: 2.0.6
171
+ version: 2.0.7
172
172
  type: :development
173
173
  prerelease: false
174
174
  version_requirements: !ruby/object:Gem::Requirement
@@ -178,7 +178,7 @@ dependencies:
178
178
  version: '2.0'
179
179
  - - ">="
180
180
  - !ruby/object:Gem::Version
181
- version: 2.0.6
181
+ version: 2.0.7
182
182
  - !ruby/object:Gem::Dependency
183
183
  name: turbo_tests2
184
184
  requirement: !ruby/object:Gem::Requirement
@@ -284,6 +284,7 @@ files:
284
284
  - exe/kettle-family
285
285
  - lib/kettle/family.rb
286
286
  - lib/kettle/family/branch_lane_audit.rb
287
+ - lib/kettle/family/branch_target_config.rb
287
288
  - lib/kettle/family/changelog_check.rb
288
289
  - lib/kettle/family/cli.rb
289
290
  - lib/kettle/family/command_result.rb
@@ -309,10 +310,10 @@ licenses:
309
310
  - AGPL-3.0-only
310
311
  metadata:
311
312
  homepage_uri: https://kettle-family.galtzo.com
312
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.16
313
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.16/CHANGELOG.md
313
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.17
314
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.17/CHANGELOG.md
314
315
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
315
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.16
316
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.17
316
317
  funding_uri: https://github.com/sponsors/pboling
317
318
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
318
319
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- d .�;����dΜ��VL���\㑤;��yIN��Q*�!��Pz e���%�Xqf�u ��KrLU�3�� )v�g���׌[���6�<h�+�!I��D��%Bmm@xv���*o5�����&}�#؛���*؟�YH4 ʳ�z����H=1�ľ_T�/a/�r�ҥ@ "}�ݹ,�lʱ�8w���u ���vSW�E*�.�m������`�&{����F�==��1F� p���i�\~+h\Q����q���u'ӴA(�ŭH�)-.�������
2
- �@s�8gT��!J7c�� ��Y���Ozٓ�}�����,dˠ�e���5�;`2j��fX-�iKD`�Z��sL���
1
+ fh�!�����R@�K[���+>���M�C�伡$qְ�EM�·6([����1<$(�m��Q���f uOV]��P���ux🪴nE�W�u�U��-�0}b>�r����i���Rދg~���+~b0������˶Giy|@�#�Ƹ3��\T:�@�?�6��k� �� gdi0�� Oq�����MI��U2y��X��d��)��HC����*���OED
2
+ ����S���t���7= 4*æ|�P��2N�&�"X�#Z.�7��>q�B��i҂�eF!* ��{o�s��ATsAu��v��/9��m��<YR�y7-��I6���-DlLT)nō��Rlٛ�ٺ�� '?0W(�ڷX��A0)��@