specguard-ruby 0.3.21 → 0.3.22

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: 9d4b951dc75ccc47535a3cf964876c26153eb72e6fcdfb2244e597b06b2c4981
4
- data.tar.gz: 5ee16e5bde0603d89334e8be374ecf831442275f8bda5b7fb9d9514dec0ba597
3
+ metadata.gz: bd0ffc9c5541eedd7981fab7f1c213c1cc2196c57465933a7d2ebcf02caf3d57
4
+ data.tar.gz: 0cc713e7625792a198fec8988f9f7f54e15fa364793cefc91dc029e3f5ba8e21
5
5
  SHA512:
6
- metadata.gz: b33f7f1e895f5b18aca3ca8b218f7d31c8962e8b0eccfbf67401c79ffc75e4b7e6502f07fd416c32703726be9091eac2ff4428404314be400f97cce56966b4e6
7
- data.tar.gz: c9eff95babda443781ad6b1147265c94660df1e1c34271169a4b7b18e0a4ac322c0b801dae059d0488d7a6180440bad7bef74852bad915dae30438c2c978b59b
6
+ metadata.gz: 616d5a1bf1568db6f3b94227273f69df69a31a6a4f6ea9af762300fc063f29ec57b5a50e95f396e1c4af221ea6917cf06c78755d916b20618c379f9630697ddb
7
+ data.tar.gz: 8c3c74d0a97ab9728ff1d02f42fb8d0bf089c4f023051139fd7f405746918c9448923e976ef1865a6b52cfaba825e53e94a562f5e76e738415fef9c176ecfaab
data/README.md CHANGED
@@ -704,6 +704,28 @@ The held-back lines are **counted and reported**, exactly as `--from-line`'s and
704
704
  the blank ones are — a summary that quietly narrowed what it was summarising
705
705
  would be worse than no summary.
706
706
 
707
+ A number you typed that the file **does not have** is the same rule pointed the
708
+ other way, and it gets its own clause rather than being left to the held-back
709
+ count. On a file that stops short of what the spec named:
710
+
711
+ ```bash
712
+ bundle exec specguard-ingest --list --lines 3,7,12-15,33-40 log/test_results.jsonl
713
+ ```
714
+
715
+ ```
716
+ line 3: branch main, commit_sha 41f2c9b8…, ci_run_id 17442, 12 examples, 8.4s
717
+
718
+ specguard-ingest: listed 6 lines from log/test_results.jsonl; 26 lines not selected by --lines; --lines named 33-40, which the file does not have; nothing was delivered
719
+ ```
720
+
721
+ A range that is only **half** answered says so on the same terms — the clause
722
+ names the portion past the end of the file, in the shorthand you typed, because
723
+ what you act on is the numbers you wrote. A line the file *does* have and that
724
+ is blank is a blank line and is reported as one; only numbers past the end of
725
+ the file reach this clause, so no line is ever named under two causes. The
726
+ clause is additive: a file that is at once short, blank-bearing and
727
+ selected-away states each cause and drops none of them.
728
+
707
729
  A spec is read strictly, and a bad one is a `2` rather than a fallback to the
708
730
  whole file — which is the one outcome a selector exists to prevent. `--lines 0`,
709
731
  `--lines 5-2`, `--lines abc`, `--lines 12-`, an empty spec and an empty entry
@@ -811,7 +833,7 @@ bundle exec specguard-ingest --json log/test_results.jsonl
811
833
  "file": "log/test_results.jsonl",
812
834
  "summary": { "lines": 3, "attempted": 3, "accepted": 2, "refused": 1,
813
835
  "undelivered": 0, "unparseable": 0, "blank": 0, "skipped": 0,
814
- "selector": null },
836
+ "absent": null, "selector": null },
815
837
  "lines": [
816
838
  { "number": 1, "status": "accepted", "code": 202, "reasons": [],
817
839
  "test_run_id": "41f2c9b8", "ci_run_id": "17442" },
@@ -839,6 +861,7 @@ bundle exec specguard-ingest --json log/test_results.jsonl
839
861
  | `summary.attempted` | how many of those were offered to the endpoint — always `0` under `--list`, and `lines` minus the unparseable ones otherwise |
840
862
  | `summary.accepted` / `refused` / `undelivered` / `unparseable` | the same four counts the text summary line states, computed once for both renderers so they cannot disagree |
841
863
  | `summary.blank` / `skipped` | the two ways a line of the file is not a row here, counted rather than dropped |
864
+ | `summary.absent` | the `--lines` numbers the file does not have, in the shorthand you typed them — a number or an `N-M` range per entry — or `null` when the selector was fully satisfied; never `[]`, on `selector`'s terms |
842
865
  | `summary.selector` | `"--lines"`, `"--from-line"`, or `null` when nothing was held back |
843
866
  | `lines[]` | one entry per row, in the file's order |
844
867
  | `foldings[]` | folding, **observed**: the lines that went out with one `ci_run_id` and came back with one `test_run_id`. The same statement the text report makes as a sentence |
@@ -349,7 +349,19 @@ module SpecGuard
349
349
  # the summary can say. The blanks and the skips are counted rather than
350
350
  # dropped because a summary that quietly narrows what it is summarising is
351
351
  # the failure this project keeps finding.
352
- Source = Struct.new(:path, :lines, :blank, :skipped, :selector, keyword_init: true)
352
+ #
353
+ # `absent` is the same discipline applied one layer later, to the lines
354
+ # that were *typed* rather than to the lines that were read. A `--lines`
355
+ # entry can name a line past the end of the file, and such a number is not
356
+ # held back — there was nothing there to hold — so `skipped` cannot carry
357
+ # it and the file's own counts cannot state it. It is derived from the
358
+ # file's length in {#read_source} and rendered in the shorthand it was
359
+ # typed in, because the user acts on what they typed. Empty when the
360
+ # selector was fully satisfied, and empty under `--from-line`, whose
361
+ # past-the-end case is already a suffix that selected nothing.
362
+ #
363
+ # @return [Array<String>] `absent`, each entry a number or an `N-M` range
364
+ Source = Struct.new(:path, :lines, :blank, :skipped, :absent, :selector, keyword_init: true)
353
365
 
354
366
  # What the command line asked for. A struct rather than a bare path,
355
367
  # because `--from-line` is the second half of the same question — which
@@ -550,6 +562,7 @@ module SpecGuard
550
562
 
551
563
  parts << blank_clause(source) if source.blank.positive?
552
564
  parts << skipped_clause(source) if source.skipped.positive?
565
+ parts << absent_clause(source) if source.absent.any?
553
566
  parts << "nothing was delivered"
554
567
 
555
568
  parts.join("; ")
@@ -655,6 +668,12 @@ module SpecGuard
655
668
  lines = []
656
669
  blank = 0
657
670
  skipped = 0
671
+ # The file's length, counted rather than inferred: `with_index` is
672
+ # already running and discards its last `number`, and a counter that
673
+ # starts at 0 says "no lines" about an empty file where a nil would say
674
+ # "not known". It is what {#absent_entries} measures the typed numbers
675
+ # against.
676
+ length = 0
658
677
 
659
678
  # Numbered from the file, not from the payloads: a blank line still
660
679
  # advances the count, so line 12 in this report is line 12 in an editor.
@@ -670,6 +689,8 @@ module SpecGuard
670
689
  # the file still delivers. Swallowing it here would lose a line silently
671
690
  # and letting it raise would report a bug in this tool.
672
691
  File.foreach(path).with_index(1) do |text, number|
692
+ length = number
693
+
673
694
  if held_back?(number, options)
674
695
  skipped += 1
675
696
  elsif text.valid_encoding? && text.strip.empty?
@@ -680,6 +701,7 @@ module SpecGuard
680
701
  end
681
702
 
682
703
  Source.new(path: path, lines: lines, blank: blank, skipped: skipped,
704
+ absent: absent_entries(options, length),
683
705
  selector: options.line_set ? :line_set : :from_line)
684
706
  rescue SystemCallError, IOError => e
685
707
  raise UsageError, "could not read #{path}: #{e.message}"
@@ -695,6 +717,39 @@ module SpecGuard
695
717
  number < options.from_line
696
718
  end
697
719
 
720
+ # The typed numbers the file does not have, in the shorthand they were
721
+ # typed in.
722
+ #
723
+ # Computed over line NUMBERS rather than over entries, which is what makes
724
+ # a half-satisfied range the same fact as a wholly unsatisfied one: a
725
+ # range running past the end of the file has named lines that were typed
726
+ # and delivered nothing, exactly as a lone out-of-range number has. Each
727
+ # entry contributes only the portion above the file's length, so the
728
+ # clause names the unanswered part rather than the entry that was half
729
+ # honoured, and a range stays a range rather than becoming a list of
730
+ # numbers the user would have to read back into what they wrote.
731
+ #
732
+ # `--from-line` contributes nothing: it is a suffix, and a suffix past the
733
+ # end of the file already says so through the lines it held back.
734
+ #
735
+ # A line the file HAS and that is blank is not here and must never be —
736
+ # `blank` already says "you named a line that exists and holds nothing",
737
+ # and stating both would be one line reported under two causes. The
738
+ # comparison is against the file's length alone, which is what keeps that
739
+ # true rather than merely usually true.
740
+ #
741
+ # @return [Array<String>]
742
+ def absent_entries(options, length)
743
+ return [] unless options.line_set
744
+
745
+ options.line_set.filter_map do |range|
746
+ first = [range.first, length + 1].max
747
+ next if first > range.last
748
+
749
+ first == range.last ? first.to_s : "#{first}-#{range.last}"
750
+ end.uniq
751
+ end
752
+
698
753
  # The per-line report on stdout — it is the product — with the diagnostics
699
754
  # about this tool's own situation on stderr, which is the split
700
755
  # `specguard-lint` already makes. `--json` moves the product and leaves the
@@ -748,10 +803,14 @@ module SpecGuard
748
803
  # is empty". A `--from-line` past the end of the file, a `--lines` that
749
804
  # names only lines the file does not have, and a genuinely empty file are
750
805
  # the same silence otherwise, and only two of them are the user's mistake.
806
+ # The third clause is what tells the second of those from the first: a
807
+ # held-back count alone is the file's own length read back, which a reader
808
+ # can mistake for a selector that did something.
751
809
  def empty_detail(source)
752
810
  parts = []
753
811
  parts << "#{source.blank} blank line#{'s' unless source.blank == 1}" if source.blank.positive?
754
812
  parts << skipped_clause(source) if source.skipped.positive?
813
+ parts << absent_clause(source) if source.absent.any?
755
814
 
756
815
  parts.empty? ? "" : " (#{parts.join('; ')})"
757
816
  end
@@ -768,6 +827,16 @@ module SpecGuard
768
827
  "#{count} earlier line#{'s' unless count == 1} skipped by --from-line"
769
828
  end
770
829
 
830
+ # The typed numbers the file does not have, named rather than counted.
831
+ # {#skipped_clause}'s counterpart for the other direction: that one says
832
+ # how much of the FILE the selector held back, and this one says how much
833
+ # of the SELECTOR the file could not answer. A count would be the wrong
834
+ # shape here — the reader's next action is editing the numbers they typed,
835
+ # so the clause hands those numbers back in the form they wrote them.
836
+ def absent_clause(source)
837
+ "--lines named #{source.absent.join(', ')}, which the file does not have"
838
+ end
839
+
771
840
  def blank_clause(source)
772
841
  "#{source.blank} blank line#{'s' unless source.blank == 1} skipped"
773
842
  end
@@ -806,6 +875,7 @@ module SpecGuard
806
875
  parts << "#{counts[:unparseable]} could not be parsed" if counts[:unparseable]
807
876
  parts << blank_clause(source) if source.blank.positive?
808
877
  parts << skipped_clause(source) if source.skipped.positive?
878
+ parts << absent_clause(source) if source.absent.any?
809
879
 
810
880
  parts.join("; ")
811
881
  end
@@ -105,7 +105,8 @@ module SpecGuard
105
105
  STATUS_LISTED = "listed"
106
106
  STATUS_UNPARSEABLE = "unparseable"
107
107
 
108
- # @param source [IngestCLI::Source] the file, its blanks and its skips
108
+ # @param source [IngestCLI::Source] the file, its blanks, its skips and
109
+ # the typed lines it does not have
109
110
  # @param results [Array<IngestCLI::LineResult>] one per line delivered,
110
111
  # in the file's order
111
112
  # @param counts [Hash{Symbol=>Integer}] status counts, computed once by
@@ -167,6 +168,15 @@ module SpecGuard
167
168
  # here. They are stated always, and for the reason {IngestCLI::Source}
168
169
  # counts rather than drops them: a summary that quietly narrows what it is
169
170
  # summarising is the failure this command is arranged against.
171
+ #
172
+ # `absent` is that same rule pointed the other way — at the numbers that
173
+ # were typed rather than at the lines that were read. A `--lines` entry
174
+ # naming past the end of the file is held back by nothing and read as
175
+ # nothing, so `skipped` (which counts lines of <file>) structurally cannot
176
+ # carry it, and without this key a satisfied selector and a phantom one
177
+ # render the identical document. It is `null` rather than `[]` where the
178
+ # selector was fully satisfied, on {#selector}'s terms: a fact that does
179
+ # not apply is absent, never a fabricated empty.
170
180
  def self.summary(source, lines:, counts:, attempted:)
171
181
  {
172
182
  "lines" => lines,
@@ -177,10 +187,21 @@ module SpecGuard
177
187
  "unparseable" => counts.fetch(:unparseable, 0),
178
188
  "blank" => source.blank,
179
189
  "skipped" => source.skipped,
190
+ "absent" => absent(source),
180
191
  "selector" => selector(source)
181
192
  }
182
193
  end
183
194
 
195
+ # The typed line numbers the file does not have, in the shorthand they
196
+ # were typed in and computed by {IngestCLI} once for both renderers — the
197
+ # discipline the counts above are held to, applied to the one fact the
198
+ # text summary and this document could otherwise disagree about.
199
+ #
200
+ # @return [Array<String>, nil]
201
+ def self.absent(source)
202
+ source.absent.empty? ? nil : source.absent
203
+ end
204
+
184
205
  # Every line that reached the endpoint, which is every line except the
185
206
  # ones that were never a run.
186
207
  def self.attempted(counts)
@@ -275,7 +296,8 @@ module SpecGuard
275
296
  Array(values).filter_map { |value| value.scrub("") if value.is_a?(String) }
276
297
  end
277
298
 
278
- private_class_method :document, :summary, :attempted, :selector, :delivered, :listed, :folded, :reasons
299
+ private_class_method :document, :summary, :attempted, :absent, :selector, :delivered, :listed, :folded,
300
+ :reasons
279
301
  end
280
302
  end
281
303
  end
@@ -45,13 +45,27 @@ module SpecGuard
45
45
  # that matters more — a missing file does not stop either tool checking
46
46
  # the good files named beside it.
47
47
  #
48
- # A path that exists and is NOT A REGULAR FILE lands in the same rescue
49
- # and is the same ratified difference one step further out: this rescue
50
- # has an errno and reports it (`Is a directory @ io_fread - <path>`),
51
- # while the binary's glob filters non-regular matches away and answers
52
- # exactly as it does for a name matching nothing. Ruby tells the two
53
- # apart and the backend cannot asserted under "a path that is not a
54
- # regular file" in that same spec.
48
+ # A path that EXISTS and is not a regular file is the same ratified
49
+ # difference one step further out. The binary's arguments are glob
50
+ # PATTERNS whose matches are filtered to regular files, so such a path
51
+ # folds into the answer a name matching nothing gets the backend has
52
+ # nothing else to say about it. This linter does no globbing and hands
53
+ # the path to `File.read` as given, so what comes back is whatever
54
+ # reading THAT path does, and never what a nonexistent path gets: the
55
+ # path is there to be opened, so the failure it does or does not produce
56
+ # is a fact about the path rather than about a pattern. Ruby tells the
57
+ # two apart and the backend cannot — asserted under "a path that is not
58
+ # a regular file" in that same spec, whose subject is a path that
59
+ # exists, is not a regular file, and is not a directory.
60
+ #
61
+ # A DIRECTORY is NOT an example of this difference, on either side.
62
+ # `CLI#select` refuses one named as an explicit path (SPGD-1303) and
63
+ # `--changed` derives its list from git, so a directory is settled
64
+ # before this rescue is reached and never reaches the backend either.
65
+ # What the shared binary makes of a bare directory argument has moved
66
+ # upstream more than once and is deliberately not restated here: the
67
+ # difference above is a property of the two sides' path semantics, and
68
+ # nothing about a directory is needed to state it.
55
69
  def scan_file(path)
56
70
  begin
57
71
  text = File.read(path, encoding: "UTF-8")
@@ -7,5 +7,5 @@
7
7
  # framework-scoped version constant a lie, so the number moved here and the
8
8
  # rspec module now points at it for back-compat.
9
9
  module SpecGuard
10
- VERSION = "0.3.21"
10
+ VERSION = "0.3.22"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: specguard-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.21
4
+ version: 0.3.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - specguard Agent
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-20 00:00:00.000000000 Z
11
+ date: 2026-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json