specguard-ruby 0.3.20 → 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 +4 -4
- data/README.md +35 -3
- data/lib/specguard/rspec/cli.rb +22 -0
- data/lib/specguard/rspec/ingest_cli.rb +71 -1
- data/lib/specguard/rspec/ingest_reporter.rb +24 -2
- data/lib/specguard/rspec/scanner.rb +21 -7
- data/lib/specguard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd0ffc9c5541eedd7981fab7f1c213c1cc2196c57465933a7d2ebcf02caf3d57
|
|
4
|
+
data.tar.gz: 0cc713e7625792a198fec8988f9f7f54e15fa364793cefc91dc029e3f5ba8e21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 616d5a1bf1568db6f3b94227273f69df69a31a6a4f6ea9af762300fc063f29ec57b5a50e95f396e1c4af221ea6917cf06c78755d916b20618c379f9630697ddb
|
|
7
|
+
data.tar.gz: 8c3c74d0a97ab9728ff1d02f42fb8d0bf089c4f023051139fd7f405746918c9448923e976ef1865a6b52cfaba825e53e94a562f5e76e738415fef9c176ecfaab
|
data/README.md
CHANGED
|
@@ -95,7 +95,14 @@ genuinely bad ref. A selection that comes up empty stays exit `0` and says WHY
|
|
|
95
95
|
on stderr — the exit contract is unchanged; only the WHY is truthful now.
|
|
96
96
|
|
|
97
97
|
Files are **positional** (`specguard-lint spec/order_spec.rb`); there is no `--source` flag —
|
|
98
|
-
that belongs to `validate-intent`, not to this one.
|
|
98
|
+
that belongs to `validate-intent`, not to this one. Named paths must be **files**: a directory
|
|
99
|
+
named as a positional argument is a usage error (exit `2`, `<path> is a directory; name files or
|
|
100
|
+
run without paths`). The `validate-intent` binary would expand it as `DIR/**`, but its report
|
|
101
|
+
carries only findings, so a clean file in that subtree appears nowhere and the linter could not
|
|
102
|
+
honestly say what it checked — the run would contradict itself, counting one selected file beside
|
|
103
|
+
several annotations and naming the directory as carrying none. Name the files, or pass no paths
|
|
104
|
+
at all and let the walker select them. A path that does *not* exist is not refused here: it
|
|
105
|
+
reaches the binary and comes back as a read finding, which is where a typo belongs.
|
|
99
106
|
|
|
100
107
|
The linter is a thin CLI over the [`validate-intent`](https://github.com/yatfa-ai/open-test-intent)
|
|
101
108
|
binary — the protocol's own reference implementation, which decides whether an annotation is valid
|
|
@@ -316,7 +323,9 @@ provenance line says which kind of "could not check" it was, in its own words:
|
|
|
316
323
|
The gem's arguments are paths; the binary's are glob patterns. A path that matches nothing —
|
|
317
324
|
missing, or not a regular file — reaches the gem as the same answer the binary gives both shapes
|
|
318
325
|
(`no-match`), which the CLI re-words as `could not read file: no file at this path` and folds into
|
|
319
|
-
the `read` kind.
|
|
326
|
+
the `read` kind. A **directory** is not one of those shapes: the client refuses it up front (see
|
|
327
|
+
"Files are positional" above), so it never reaches the binary and never becomes a read finding.
|
|
328
|
+
The binary's own UTF-8 refusal prose (`input is not well-formed UTF-8
|
|
320
329
|
(PROTOCOL.md §1.1 requires it)`) and parse-failure prose are passed through unaltered.
|
|
321
330
|
|
|
322
331
|
Every way the backend can fail — the binary is missing, will not execute, exits with something that
|
|
@@ -695,6 +704,28 @@ The held-back lines are **counted and reported**, exactly as `--from-line`'s and
|
|
|
695
704
|
the blank ones are — a summary that quietly narrowed what it was summarising
|
|
696
705
|
would be worse than no summary.
|
|
697
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
|
+
|
|
698
729
|
A spec is read strictly, and a bad one is a `2` rather than a fallback to the
|
|
699
730
|
whole file — which is the one outcome a selector exists to prevent. `--lines 0`,
|
|
700
731
|
`--lines 5-2`, `--lines abc`, `--lines 12-`, an empty spec and an empty entry
|
|
@@ -802,7 +833,7 @@ bundle exec specguard-ingest --json log/test_results.jsonl
|
|
|
802
833
|
"file": "log/test_results.jsonl",
|
|
803
834
|
"summary": { "lines": 3, "attempted": 3, "accepted": 2, "refused": 1,
|
|
804
835
|
"undelivered": 0, "unparseable": 0, "blank": 0, "skipped": 0,
|
|
805
|
-
"selector": null },
|
|
836
|
+
"absent": null, "selector": null },
|
|
806
837
|
"lines": [
|
|
807
838
|
{ "number": 1, "status": "accepted", "code": 202, "reasons": [],
|
|
808
839
|
"test_run_id": "41f2c9b8", "ci_run_id": "17442" },
|
|
@@ -830,6 +861,7 @@ bundle exec specguard-ingest --json log/test_results.jsonl
|
|
|
830
861
|
| `summary.attempted` | how many of those were offered to the endpoint — always `0` under `--list`, and `lines` minus the unparseable ones otherwise |
|
|
831
862
|
| `summary.accepted` / `refused` / `undelivered` / `unparseable` | the same four counts the text summary line states, computed once for both renderers so they cannot disagree |
|
|
832
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 |
|
|
833
865
|
| `summary.selector` | `"--lines"`, `"--from-line"`, or `null` when nothing was held back |
|
|
834
866
|
| `lines[]` | one entry per row, in the file's order |
|
|
835
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 |
|
data/lib/specguard/rspec/cli.rb
CHANGED
|
@@ -248,6 +248,28 @@ module SpecGuard
|
|
|
248
248
|
"(named files are checked as given, --changed derives them from the diff)"
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
+
# SPGD-1303: a directory named as an explicit path is refused, not
|
|
252
|
+
# expanded. The shared `validate-intent` binary learned to expand a
|
|
253
|
+
# bare directory as `DIR/**` (oti SPGD-1299), so one argument stopped
|
|
254
|
+
# meaning one file — and the client's document carries only FINDINGS,
|
|
255
|
+
# so a clean file in that subtree appears nowhere and "checked N spec
|
|
256
|
+
# files" can never count what was actually validated. The observed
|
|
257
|
+
# result was a run whose header said `checked 1 spec file`, whose
|
|
258
|
+
# summary said `checked 3 @intent annotations`, and whose
|
|
259
|
+
# zero-annotation note claimed the directory carried none — three
|
|
260
|
+
# mutually contradicting sentences, and at its sharpest an exit-0
|
|
261
|
+
# false green. Honest accounting is impossible here without an oti
|
|
262
|
+
# wire-contract change, so refusal is the only coherent client-side
|
|
263
|
+
# state. Same sentence as the ts client's `discover.ts` explicit arm.
|
|
264
|
+
# `File.directory?` follows symlinks, so a symlinked directory is the
|
|
265
|
+
# same class; a nonexistent path is NOT a directory and still reaches
|
|
266
|
+
# the binary, where the no-match finding belongs.
|
|
267
|
+
options[:files].each do |path|
|
|
268
|
+
next unless File.directory?(path)
|
|
269
|
+
|
|
270
|
+
raise UsageError, "#{path} is a directory; name files or run without paths"
|
|
271
|
+
end
|
|
272
|
+
|
|
251
273
|
return FileSelector::Selection.new(files: options[:files], mode: :explicit)
|
|
252
274
|
end
|
|
253
275
|
|
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
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")
|
data/lib/specguard/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|