pray-cli 1.26.0 → 1.26.3

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: '087854aaa5f5bfead826eaa843615d2a52ba5f52236cc953b55d715c66c6f84b'
4
- data.tar.gz: 2600fce7bf405cdcc5f9ef78da7e7026dd408f0babd026ecbf869dc09da2de6f
3
+ metadata.gz: 2c518cdb4d98021a08d150ad7c0c20463d885b09c9f69b022f2fa596d9597269
4
+ data.tar.gz: 8e46339135b9a8cb554eb4b8d9f60510baa5eab1b1b730bbda70efdd227289f3
5
5
  SHA512:
6
- metadata.gz: 1242427bff2198aafc4c74e2c6977128150dde7ba0d7fadecf9d92df8bcaf68662028f45e11419faf8e3a25b9b3c9c959034190dff054cb264c93b303ee1fe3a
7
- data.tar.gz: 8d4b9ebc38d9fef0360ec9173b7e53ff92baff22f146e349f6ab049b48b1d0668883bfa0acb6bd3c8a8fd42993e91d06bc96b310b58df09d83dcfa0c377899f8
6
+ metadata.gz: 1b619b7f79ee252328c885ac0664270d29f00f81a720102a26f8a1af8d78c57e7f9245f6eb09e724a3ba1f19d482001c59a8153d0f2da534d64fe64cdc5cc314
7
+ data.tar.gz: bd6f8f108e1e8cab3e48060c09dff86b8dfc55cc5edaaed320fcb6d347f2b9ea90e208ceb5c2757136084b7da815f34b491ef7fffec3f78d7b70abfb530819d3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.26.3 (2026-11-12)
6
+
7
+ - Allow `pray install` to refresh the lock when a managed span already matches the fresh render but the lock ideal is stale. Keep failing when the on-disk body matches neither, and name move-aside recovery instead of a force flag.
8
+
9
+ ## 1.26.2 (2026-11-06)
10
+
11
+ - Reduce wall and CPU time for package resolve, tree hashing, and related install work without changing install results.
12
+
13
+ ## 1.26.1 (2026-11-02)
14
+
15
+ - Show optional package referrals separately in `pray explain`, exclude them from dependency cycles and trees, and validate their constraints when another declaration selects the target.
16
+ - Keep drift, render checks, planning, and writes consistent for managed files that contain local content.
17
+
5
18
  ## 1.26.0 (2026-10-30)
6
19
 
7
20
  - Add validated `related` references to prayspec parsing, rendering, and `pray explain`.
@@ -84,7 +84,12 @@ module Pray
84
84
  end
85
85
  lines << "source: #{package_source_summary(package)}"
86
86
  lines << "exports: #{format_list(package.selected_exports)}"
87
- lines << "dependencies: #{format_list(package.spec.dependencies.map(&:name))}"
87
+ required_dependencies = package.spec.dependencies.reject(&:optional).map(&:name)
88
+ optional_referrals = package.spec.dependencies.select(&:optional).map do |dependency|
89
+ "#{dependency.name} (#{dependency.constraint})"
90
+ end
91
+ lines << "dependencies: #{format_list(required_dependencies)}"
92
+ lines << "optional referrals: #{format_list(optional_referrals)}"
88
93
  lines << "related prayers: #{format_list(package.spec.related)}"
89
94
  lines << "tree hash: #{package.tree_hash}"
90
95
  lines << "artifact hash: #{package.artifact_hash}"
@@ -152,7 +152,7 @@ module Pray
152
152
  lines << "#{indent}#{package.declaration.name} #{package.spec.recorded_version}"
153
153
  return unless ancestry.add?(package.declaration.name)
154
154
 
155
- package.spec.dependencies.each do |dependency|
155
+ package.spec.dependencies.reject(&:optional).each do |dependency|
156
156
  resolved = package_map[dependency.name]
157
157
  if resolved
158
158
  if ancestry.include?(resolved.declaration.name)
data/lib/pray/hashing.rb CHANGED
@@ -24,6 +24,8 @@ module Pray
24
24
  end
25
25
 
26
26
  def normalize_line_endings(text)
27
+ return text unless text.include?("\r")
28
+
27
29
  text.gsub("\r\n", "\n").tr("\r", "\n")
28
30
  end
29
31
 
@@ -54,27 +54,6 @@ module Pray
54
54
  copy.dependencies = dependencies.sort_by { |dependency| [dependency.name, dependency.constraint, dependency.optional] }
55
55
  end
56
56
  end
57
-
58
- def tree_hash_for_root(root)
59
- validate_relative_paths!
60
- file_bytes = {}
61
- files.each do |file|
62
- path = File.join(root, file)
63
- raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
64
- raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
65
-
66
- file_bytes[file] = File.binread(path)
67
- end
68
- self.class.tree_hash_from_file_bytes(file_bytes)
69
- end
70
-
71
- def self.tree_hash_from_file_bytes(file_bytes)
72
- entries = file_bytes.map { |path, bytes| [path, Hashing.sha256_prefixed(bytes)] }.sort_by(&:first)
73
- serialized = entries.map do |path, hash|
74
- "file\0regular\0#{path}\0#{hash}\n"
75
- end.join
76
- Hashing.sha256_prefixed(serialized)
77
- end
78
57
  end
79
58
 
80
59
  module PackageSpecParser
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Pray
6
+ class PackageSpec
7
+ def tree_hash_for_root(root)
8
+ validate_relative_paths!
9
+ file_bytes = {}
10
+ files.each do |file|
11
+ path = File.join(root, file)
12
+ raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
13
+ raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
14
+
15
+ file_bytes[file] = File.binread(path)
16
+ end
17
+ self.class.tree_hash_from_file_bytes(file_bytes)
18
+ end
19
+
20
+ def self.tree_hash_from_file_bytes(file_bytes)
21
+ # Stream the same bytes the prior string builder emitted so the digest stays identical.
22
+ digest = Digest::SHA256.new
23
+ file_bytes.keys.sort.each do |path|
24
+ bytes = file_bytes.fetch(path)
25
+ digest << "file\0regular\0"
26
+ digest << path
27
+ digest << "\0"
28
+ digest << "sha256:"
29
+ digest << Digest::SHA256.hexdigest(bytes)
30
+ digest << "\n"
31
+ end
32
+ Hashing.prefixed_hex_digest(digest.digest)
33
+ end
34
+ end
35
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
- require "pathname"
5
4
 
6
5
  module Pray
7
6
  module RenderDest
@@ -157,7 +156,7 @@ module Pray
157
156
  unless kind == :regular
158
157
  raise Error.render("refusing to write `#{display}`; destination is not a regular file")
159
158
  end
160
- RenderPatch.patch_rendered_content(decode_utf8(read_regular_bytes(path, display), display), fresh, retired)
159
+ RenderReconcile.reconcile_content(decode_utf8(read_regular_bytes(path, display), display), fresh, retired)
161
160
  end
162
161
 
163
162
  def write_rendered_content(path, display, fresh, retired = {})
@@ -165,7 +164,7 @@ module Pray
165
164
 
166
165
  open_regular(path, display, File::RDWR) do |file|
167
166
  existing = decode_utf8(read_destination_bytes(file, display), display)
168
- content = RenderPatch.patch_rendered_content(existing, fresh, retired)
167
+ content = RenderReconcile.reconcile_content(existing, fresh, retired)
169
168
  return if Transaction.replace(path, existing.b, content.b)
170
169
 
171
170
  file.rewind
@@ -4,6 +4,10 @@ module Pray
4
4
  module RenderReconcile
5
5
  module_function
6
6
 
7
+ def reconcile_content(existing, fresh, retired = {})
8
+ RenderPatch.patch_rendered_content(existing, fresh, retired)
9
+ end
10
+
7
11
  def current?(path, display, fresh, retired = {})
8
12
  kind = RenderDest.destination_kind(path)
9
13
  return false if kind == :missing
@@ -13,7 +17,7 @@ module Pray
13
17
  raise Error.render("refusing to read `#{display}`; destination is not a regular file")
14
18
  end
15
19
  existing = RenderDest.decode_utf8(RenderDest.read_regular_bytes(path, display), display)
16
- existing == RenderPatch.patch_rendered_content(existing, fresh, retired)
20
+ existing == reconcile_content(existing, fresh, retired)
17
21
  end
18
22
 
19
23
  def retired_span_checksums(previous_lockfile, target)
@@ -27,6 +31,9 @@ module Pray
27
31
  end
28
32
 
29
33
  def reject_managed_span_conflicts!(project, rendered, previous_lockfile)
34
+ fresh_ideals = rendered.each_with_object({}) do |target, ideals|
35
+ target.managed_spans.each { |span| ideals[span.id] = span.ideal_checksum }
36
+ end
30
37
  active_by_target = rendered.to_h do |target|
31
38
  [target.path, target.managed_spans.to_h { |span| [span.id, true] }]
32
39
  end
@@ -43,11 +50,16 @@ module Pray
43
50
  bodies = RenderPatch.managed_bodies(RenderPatch.split_segments(content))
44
51
  spans.each do |span|
45
52
  next unless active[span.id] && bodies.key?(span.id)
46
- next if Hashing.checksum_managed_span_content(bodies[span.id]) == span.ideal_checksum
53
+
54
+ checksum = Hashing.checksum_managed_span_content(bodies[span.id])
55
+ next if checksum == span.ideal_checksum
56
+ next if checksum == fresh_ideals[span.id]
47
57
 
48
58
  raise Error.render(
49
- "conflict: managed span `#{span.id}` in `#{target_path}` was edited " \
50
- "(`#{span.package}::#{span.export}`)"
59
+ "conflict: managed span `#{span.id}` in `#{target_path}` differs from lock and from " \
60
+ "fresh render (`#{span.package}::#{span.export}`).\n" \
61
+ "Move the destination aside and rerun `pray install`, or restore the managed block " \
62
+ "to the locked bytes."
51
63
  )
52
64
  end
53
65
  end
data/lib/pray/resolve.rb CHANGED
@@ -100,6 +100,7 @@ module Pray
100
100
  raise Error.resolution(local_errors.join("\n")) unless local_errors.empty?
101
101
 
102
102
  ResolveDeps.reject_dependency_cycles(packages)
103
+ ResolveDeps.validate_optional_referrals(packages)
103
104
  source_revisions = git_sources.revisions
104
105
 
105
106
  ResolvedProject.new(
@@ -276,18 +277,17 @@ module Pray
276
277
  end
277
278
 
278
279
  def find_prayspec_file(root)
279
- files = []
280
+ found = nil
280
281
  Dir.each_child(root) do |entry|
281
282
  next unless File.extname(entry) == ".prayspec"
282
283
 
283
284
  path = File.join(root, entry)
284
- files << path if File.file?(path)
285
- end
286
- case files.length
287
- when 1 then files.first
288
- when 0 then raise Error.resolution("no prayspec file found in #{root.inspect}")
289
- else raise Error.resolution("multiple prayspec files found in #{root.inspect}")
285
+ next unless File.file?(path)
286
+ raise Error.resolution("multiple prayspec files found in #{root.inspect}") if found
287
+
288
+ found = path
290
289
  end
290
+ found || raise(Error.resolution("no prayspec file found in #{root.inspect}"))
291
291
  end
292
292
 
293
293
  def source_map(sources)
@@ -5,14 +5,35 @@ module Pray
5
5
  module_function
6
6
 
7
7
  def reject_dependency_cycles(packages)
8
+ return if packages.all? { |package| package.spec.dependencies.all?(&:optional) }
9
+
8
10
  edges = {}
9
11
  packages.each do |package|
10
- edges[package.declaration.name] = package.spec.dependencies.map(&:name)
12
+ edges[package.declaration.name] = package.spec.dependencies.reject(&:optional).map(&:name)
11
13
  end
12
14
  cycle = DependencyGraph.find_dependency_cycle(edges)
13
15
  return unless cycle
14
16
 
15
17
  raise Error.resolution("dependency cycle detected: #{cycle.join(" -> ")}")
16
18
  end
19
+
20
+ def validate_optional_referrals(packages)
21
+ return if packages.all? { |package| package.spec.dependencies.none?(&:optional) }
22
+
23
+ packages_by_name = packages.to_h { |package| [package.declaration.name, package] }
24
+ packages.each do |package|
25
+ package.spec.dependencies.select(&:optional).each do |dependency|
26
+ target = packages_by_name[dependency.name]
27
+ next unless target
28
+
29
+ target.spec.satisfy_constraint!(dependency.constraint)
30
+ rescue Error
31
+ raise Error.resolution(
32
+ "optional referral #{package.declaration.name} -> #{dependency.name} requires " \
33
+ "#{dependency.constraint}, but selected version is #{target.spec.recorded_version}"
34
+ )
35
+ end
36
+ end
37
+ end
17
38
  end
18
39
  end
data/lib/pray/verify.rb CHANGED
@@ -36,11 +36,16 @@ module Pray
36
36
 
37
37
  def drift_project(project, lockfile)
38
38
  report, rendered_targets, fresh_targets = collect_verification_report(project, lockfile)
39
- fresh_targets.each do |path, fresh_content|
40
- normalized_fresh = Hashing.normalize_line_endings(fresh_content)
39
+ fresh_targets.each do |path, fresh_target|
41
40
  on_disk = rendered_targets[path]
42
- on_disk = Hashing.normalize_line_endings(on_disk) if on_disk
43
- unless on_disk == normalized_fresh
41
+ retired = RenderReconcile.retired_span_checksums(lockfile, fresh_target)
42
+ expected = if on_disk
43
+ RenderReconcile.reconcile_content(on_disk, fresh_target.content, retired)
44
+ else
45
+ fresh_target.content
46
+ end
47
+ normalized_on_disk = Hashing.normalize_line_endings(on_disk) if on_disk
48
+ unless normalized_on_disk == Hashing.normalize_line_endings(expected)
44
49
  report.findings << VerificationFinding.new(
45
50
  kind: "renderer_drift",
46
51
  message: "#{path} differs from fresh render"
@@ -64,7 +69,7 @@ module Pray
64
69
  def collect_verification_report(project, lockfile)
65
70
  report = VerificationReport.new
66
71
  rendered_targets = {}
67
- fresh_targets = Render.render_project(project).to_h { |target| [target.path, target.content] }
72
+ fresh_targets = Render.render_project(project).to_h { |target| [target.path, target] }
68
73
 
69
74
  if project.manifest_hash != lockfile.manifest_hash
70
75
  report.findings << VerificationFinding.new(
@@ -104,6 +109,7 @@ module Pray
104
109
  end
105
110
 
106
111
  target_spans = lockfile.managed_span.group_by(&:target)
112
+ fresh_targets.each_key { |target_path| target_spans[target_path] ||= [] }
107
113
  target_spans.each do |target_path, spans|
108
114
  absolute_path = File.join(project.project_root, target_path)
109
115
  unless File.exist?(absolute_path)
@@ -135,7 +141,7 @@ module Pray
135
141
  end
136
142
  end
137
143
  end
138
- fresh_lines = fresh_targets[target_path]&.lines(chomp: true)
144
+ fresh_lines = fresh_targets[target_path]&.content&.lines(chomp: true)
139
145
  summary = VerifyPosition.summarize_position_drift(
140
146
  target_path,
141
147
  spans,
data/lib/pray/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pray
4
- VERSION = "1.26.0"
5
- GENERATED_BY = "pray 1.26.0"
4
+ VERSION = "1.26.3"
5
+ GENERATED_BY = "pray 1.26.3"
6
6
  end
data/lib/pray.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "pray/format_manifest"
18
18
  require_relative "pray/package_relation"
19
19
  require_relative "pray/package_spec"
20
20
  require_relative "pray/package_spec_paths"
21
+ require_relative "pray/package_spec_tree_hash"
21
22
  require_relative "pray/package_spec_version"
22
23
  require_relative "pray/local_prayer"
23
24
  require_relative "pray/lockfile"
data/pray-cli.gemspec CHANGED
@@ -44,5 +44,5 @@ Gem::Specification.new do |spec|
44
44
 
45
45
  spec.add_development_dependency "rspec", "~> 3.13"
46
46
  spec.add_development_dependency "polyrun", "~> 2.2"
47
- spec.add_development_dependency "rbs", "~> 3.9"
47
+ spec.add_development_dependency "rbs", "~> 4.0"
48
48
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pray-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.0
4
+ version: 1.26.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -77,14 +77,14 @@ dependencies:
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '3.9'
80
+ version: '4.0'
81
81
  type: :development
82
82
  prerelease: false
83
83
  version_requirements: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: '3.9'
87
+ version: '4.0'
88
88
  description: 'Ruby Prayfile CLI and library: resolve, lock, render, verify, and publish
89
89
  inference input packages. Consumes path, git, and registry distribution points.'
90
90
  email:
@@ -166,6 +166,7 @@ files:
166
166
  - lib/pray/package_spec.rb
167
167
  - lib/pray/package_spec_paths.rb
168
168
  - lib/pray/package_spec_render.rb
169
+ - lib/pray/package_spec_tree_hash.rb
169
170
  - lib/pray/package_spec_version.rb
170
171
  - lib/pray/path_safety.rb
171
172
  - lib/pray/plan.rb
@@ -251,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
252
  - !ruby/object:Gem::Version
252
253
  version: '0'
253
254
  requirements: []
254
- rubygems_version: 3.6.9
255
+ rubygems_version: 4.0.20
255
256
  specification_version: 4
256
257
  summary: Package manager for the language placed before inference (Prayfile)
257
258
  test_files: []