specguard-ruby 0.3.24 → 0.3.26

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: dd1e42b8014d4627c1126b81fd4e419e000316e788f0115a24dbbf9041ad52b1
4
- data.tar.gz: d74ca48cff5263d913f727a94d59b6bb6ffd477ff4a31bddcf7d83599b96f9c3
3
+ metadata.gz: 788caaa70678a741f5ddb2aa3379d033369905ee89a7164f37527f0c64ecf3a5
4
+ data.tar.gz: 8c03300935002043fd6097d3779057ed0dec439b13108bdc440ced0fee71af95
5
5
  SHA512:
6
- metadata.gz: 79519698cb6c136909f09bd425548bdac0e3ab936145449e983937d2f9f97e9f1e30e7f3bcd9936e1c200b2c49358157d9b97469804f3b11a200538557dc78f3
7
- data.tar.gz: c034dc9514a7cb085909a1bcdfb7c51e84dd67af457bbd73c26c4a4d31f21afae6c6893a7f42d2475dd350b80d0112879698ab5a912cf2dee5c83dfa7abc7c25
6
+ metadata.gz: 138c404bb1691827932705550d146c5d34c908ef5c3581ef6f1aa6d210de46f8ba0ef349f81486bab4340ead74588858031012732f26a46e7df691ce4fce78ce
7
+ data.tar.gz: 26e5764b84b90f120caea35853e9205b1e1142bf3f3709b761f164acc7f294b2cda5aa6dcc6f89354e03f7bc750a6d3268050f1ef25961db606b6b1584e4bff8
data/README.md CHANGED
@@ -48,7 +48,8 @@ bundle exec ruby -rminitest/specguard_plugin -e 'Minitest.extensions << "specgua
48
48
 
49
49
  Validates `# @intent:` annotations in changed (or all) `*_spec.rb` files against the OpenTestIntent
50
50
  JSON Schema. Exits `1` on a malformed annotation — or a well-formed but unreachable one (stacked
51
- above another comment-form `@intent:` line, so the one-line lookback never claims it) — and
51
+ above another comment-form `@intent:` line, so the one-line lookback never claims it, or trailing
52
+ on a `describe`/`context` group line, which no example's extraction can ever reach) — and
52
53
  **never** fails on a *missing* one (adoption is opt-in and gradual).
53
54
 
54
55
  ```bash
@@ -334,7 +335,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
334
335
  is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
335
336
  my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
336
337
  but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
337
- never claims it — and nothing else.
338
+ never claims it, or trailing on a `describe`/`context` group line, which no example's extraction
339
+ can ever reach — and nothing else.
338
340
 
339
341
  ## The formatter — `SpecGuard::RSpecFormatter`
340
342
 
@@ -877,7 +879,9 @@ command draws. Everything else stays **byte for byte, in the file's order**:
877
879
 
878
880
  The rewrite is **atomic**: a temporary file in the same directory, renamed over
879
881
  the original, so a failure mid-drain leaves the file exactly as it was — and
880
- nothing is written at all unless something was actually accepted.
882
+ nothing is written at all unless something was actually accepted. The swap
883
+ also keeps the queue file's permissions, and when the queue path is a symlink
884
+ it rewrites the link's target rather than replacing the link.
881
885
 
882
886
  **The removal renumbers what it leaves.** The rewrite keeps the surviving lines
883
887
  byte for byte and in order, and packing them up from the top gives them new
@@ -126,10 +126,12 @@ module SpecGuard
126
126
  # "Lint, don't require": a missing annotation is never an error.
127
127
  EXIT_OK = 0
128
128
  # One or more annotations are malformed — or well-formed but
129
- # unreachable (SPGD-900: stacked above another comment-form `@intent:`
130
- # line, so the one-line lookback never claims it, a dead contract that
131
- # is still a failure). The only code produced by inspecting content,
132
- # and the only path that reaches it is a failed {Linter::Result}.
129
+ # unreachable (SPGD-900/SPGD-1510: stacked above another comment-form
130
+ # `@intent:` line so the one-line lookback never claims it, or trailing
131
+ # on an example-group line no example's extraction can reach — a dead
132
+ # contract either way, and still a failure). The only code produced by
133
+ # inspecting content, and the only path that reaches it is a failed
134
+ # {Linter::Result}.
133
135
  EXIT_MALFORMED = 1
134
136
  # The linter could not do its job: bad flags, `--changed` outside a git
135
137
  # repository, a validator that could not be resolved, or an unexpected
@@ -165,12 +167,14 @@ module SpecGuard
165
167
 
166
168
  results = backend.check(selection.files)
167
169
 
168
- # The structural pass (SPGD-900): annotations that are valid in
169
- # isolation but can never be extracted — stacked consecutive
170
+ # The structural pass (SPGD-900, SPGD-1510): annotations that are
171
+ # valid in isolation but can never be extracted — stacked consecutive
170
172
  # comment-form `@intent:` lines, where the one-line lookback
171
- # (SPGD-12 §2) claims only the line just above the example. These are
172
- # Linter::Results like any other, so both renderers and the exit code
173
- # pick them up with no second path to keep in step.
173
+ # (SPGD-12 §2) claims only the line just above the example, and
174
+ # `@intent:` tokens trailing on an example-group line, which no
175
+ # extraction rule can ever reach. These are Linter::Results like any
176
+ # other, so both renderers and the exit code pick them up with no
177
+ # second path to keep in step.
174
178
  results += unreachable_results(selection.files)
175
179
 
176
180
  # Computed once, here, and handed to whichever renderer runs. `--json`
@@ -476,15 +480,16 @@ module SpecGuard
476
480
  #
477
481
  # The boundary is the set-difference the partition above already
478
482
  # supports: a file counts as covered when it yielded any line-scoped
479
- # result (an annotation, valid or not — the SPGD-900 unreachable findings
480
- # are line-scoped too and require an annotation to exist, so they cover
481
- # their file) or when it could not be read at all (KIND_READ → `unread`).
482
- # An unread file is NOT a zero-annotation file: it already gets its own
483
- # summary clause and FAIL line, and calling it annotation-free would be
484
- # the same overstatement `summary_line` refuses to make. Unreachable
485
- # findings need a stacked annotation — impossible in a file with none —
486
- # so `annotations ∪ unread` already covers every non-bare file and no
487
- # third subtraction is needed.
483
+ # result (an annotation, valid or not — the SPGD-900/SPGD-1510
484
+ # unreachable findings are line-scoped too and require an annotation to
485
+ # exist, so they cover their file) or when it could not be read at all
486
+ # (KIND_READ → `unread`). An unread file is NOT a zero-annotation file:
487
+ # it already gets its own summary clause and FAIL line, and calling it
488
+ # annotation-free would be the same overstatement `summary_line` refuses
489
+ # to make. Unreachable findings need an annotation to exist — a stacked
490
+ # comment-form pair, or an `@intent:` token on a group line — impossible
491
+ # in a file with none, so `annotations ∪ unread` already covers every
492
+ # non-bare file and no third subtraction is needed.
488
493
  #
489
494
  # It is a NOTE on stderr, never a warning and never an exit code: "lint,
490
495
  # don't require" (SPGD-12 §1) keeps a missing annotation a non-error.
@@ -48,15 +48,22 @@ module SpecGuard
48
48
  # of ways an annotation can fail is written down in one place.
49
49
  KIND_SCHEMA = :schema
50
50
 
51
- # The annotation is well-formed in isolation but can never be EXTRACTED:
52
- # it is a comment-form `@intent:` on a line whose next line is also a
53
- # comment-form `@intent:`, so the one-line lookback (SPGD-12 §2 —
54
- # {AnnotationLookup} claims only the comment on the line immediately
55
- # above an example) always skips it in favour of the lower line. The
56
- # contract is dead metadata: counted by the linter, discarded by
57
- # extraction. Produced by {Scanner}'s structural pass, not by
58
- # {AnnotationScanner} — each annotation is scanned in isolation there,
59
- # which is exactly why this defect is invisible to it.
51
+ # The annotation is well-formed in isolation but can never be EXTRACTED.
52
+ # Two shapes, both dead the moment they are written:
53
+ #
54
+ # * a comment-form `@intent:` stacked above another comment-form
55
+ # `@intent:` line, so the one-line lookback (SPGD-12 §2 —
56
+ # {AnnotationLookup} claims only the comment on the line immediately
57
+ # above an example) always skips it in favour of the lower line;
58
+ # * an `@intent:` trailing on an example-group line
59
+ # ({Scanner::GROUP_LINE} — SPGD-1510): a group line is no example's
60
+ # own and is not comment-only, so no extraction rule can ever reach
61
+ # it, and the example beneath it silently ingests unannotated.
62
+ #
63
+ # Either way the contract is dead metadata: counted by the linter,
64
+ # discarded by extraction. Produced by {Scanner}'s structural pass, not
65
+ # by {AnnotationScanner} — each annotation is scanned in isolation
66
+ # there, which is exactly why these defects are invisible to it.
60
67
  KIND_UNREACHABLE = :unreachable
61
68
 
62
69
  def initialize(file:, line:, intent: nil, problem: nil, kind: nil)
@@ -1155,13 +1155,26 @@ module SpecGuard
1155
1155
  # The atomic swap: a temporary file in the SAME directory — `rename` is
1156
1156
  # only atomic within one filesystem — renamed over the original. A
1157
1157
  # failure at any point before the rename leaves the original untouched,
1158
- # and the `ensure` takes the temporary with it.
1158
+ # and the `ensure` takes the temporary with it. The rename lands on the
1159
+ # RESOLVED target (`File.realpath`), so a queue reached through a
1160
+ # symlink is rewritten at the file the link points to and the link
1161
+ # itself survives: `rename(2)` replaces the directory entry at the path
1162
+ # given to it, and without this the swap would turn the link into a
1163
+ # regular file while the real target kept every line just accepted —
1164
+ # lines the next drain would then send again. The replacement is
1165
+ # chmod'ed to the target's mode before the swap, because
1166
+ # `File.binwrite` creates it under the umask's defaults and a queue
1167
+ # restricted to 0600 would come back 0644 as a fresh inode. Ownership
1168
+ # is deliberately NOT carried over: `chown` needs privilege, and in
1169
+ # practice the drain runs as the queue's owner.
1159
1170
  def drain_write(path, content)
1160
- tmp = File.join(File.dirname(path),
1161
- ".#{File.basename(path)}.drain-#{Process.pid}-#{rand(1 << 32).to_s(36)}.tmp")
1171
+ target = File.realpath(path)
1172
+ tmp = File.join(File.dirname(target),
1173
+ ".#{File.basename(target)}.drain-#{Process.pid}-#{rand(1 << 32).to_s(36)}.tmp")
1162
1174
  begin
1163
1175
  File.binwrite(tmp, content)
1164
- File.rename(tmp, path)
1176
+ File.chmod(File.stat(target).mode & 0o7777, tmp)
1177
+ File.rename(tmp, target)
1165
1178
  ensure
1166
1179
  File.unlink(tmp) if File.exist?(tmp)
1167
1180
  end
@@ -38,12 +38,16 @@ module SpecGuard
38
38
  # `read` kind) and still reports it as `FAIL` with exit 1.
39
39
  #
40
40
  # `Finding::KIND_UNREACHABLE` — an annotation that is well-formed in
41
- # isolation but stacked above another comment-form `@intent:` line, so
42
- # the one-line lookback (SPGD-12 §2) never claims it — is also reported
43
- # as a failure, and therefore also exits 1. Nothing about it is
41
+ # isolation but can never be extracted, so the one-line lookback
42
+ # (SPGD-12 §2) never claims it — is also reported as a failure, and
43
+ # therefore also exits 1. Two shapes reach this kind: a comment-form
44
+ # `@intent:` stacked above another comment-form `@intent:` line (SPGD-900),
45
+ # and an `@intent:` trailing on an example-group line, which is no
46
+ # example's own and not comment-only (SPGD-1510). Nothing about either is
44
47
  # malformed; the contract is dead metadata, counted by the linter and
45
- # discarded by extraction, and the structural pass that flags it
46
- # (SPGD-900) reports it loudly rather than letting it pass as clean.
48
+ # discarded by extraction, and the structural pass that flags them
49
+ # (SPGD-900, SPGD-1510) reports it loudly rather than letting it pass as
50
+ # clean.
47
51
  module Linter
48
52
  # One annotation's verdict. `problem` is set when discovery could not
49
53
  # produce an intent at all; `reasons` when the schema rejected one.
@@ -258,25 +258,41 @@ module SpecGuard
258
258
  #
259
259
  # Everything above validates each annotation IN ISOLATION — one payload,
260
260
  # one verdict. But the formatter's extraction contract
261
- # ({AnnotationLookup}, SPGD-12 §2) is positional: only the comment-form
262
- # annotation on the line IMMEDIATELY ABOVE an example is inheritable.
263
- # So when two consecutive comment-form `@intent:` lines sit above one
264
- # `it`, the UPPER line is unreachable — silently discarded by the
265
- # one-line lookback — while this pipeline counts it as a valid
266
- # annotation and the run exits 0. Measured (SPGD-897 audit): 16 such
267
- # stacked pairs shipped through `specguard-lint` exit 0, every upper
268
- # contract dead. Reported loudly, not skipped — the same stance
261
+ # ({AnnotationLookup}, SPGD-12 §2) is positional: an annotation reaches
262
+ # an example only through its own line (the trailing form, and only when
263
+ # that line IS the example's) or through the comment-form line
264
+ # IMMEDIATELY ABOVE it. Two shapes fall outside both and are dead the
265
+ # moment they are written — silently discarded by extraction while this
266
+ # pipeline counts them as valid annotations and the run exits 0:
267
+ #
268
+ # * the STACKED pair (SPGD-897, found in the audit): two consecutive
269
+ # comment-form `@intent:` lines above one `it` — the UPPER line is
270
+ # never claimed, because the one-line lookback takes only the line
271
+ # just above the example. Measured: 16 such stacked pairs shipped
272
+ # through `specguard-lint` exit 0, every upper contract dead.
273
+ # * the GROUP line (SPGD-1510, cost SPGD-1475 a full rework round): an
274
+ # `@intent:` trailing on a `describe`/`context`/… line. The line is
275
+ # no example's own, and it is not comment-only, so no lookback can
276
+ # ever claim it — the example beneath it silently ingests as
277
+ # unannotated. A describe insertion that swallowed a newline is how
278
+ # the shape is born.
279
+ #
280
+ # Both are reported loudly, not skipped — the same stance
269
281
  # {AnnotationScanner} takes for NO_PAYLOAD.
270
282
  #
271
283
  # A comment-form annotation line is a comment-only line
272
284
  # ({AnnotationLookup::COMMENT_LINE}) carrying the `@intent:` token. The
273
- # trailing same-line form never matches COMMENT_LINE, so it is never
274
- # flagged and never makes a neighbour unreachable — its annotation
275
- # belongs to its own example's line and the lookback is not involved.
285
+ # trailing same-line form is claimed ONLY when the line itself defines
286
+ # an example ({EXAMPLE_LINE}); there the annotation belongs to its own
287
+ # example's line and the lookback is not involved. On any other line the
288
+ # trailing form is dead too — an example-group line carrying it is now
289
+ # flagged by {GROUP_LINE}'s pass below, with the one-liner exemption
290
+ # ({EXAMPLE_ON_LINE}) keeping `describe "x" do it("y") { } end` claims
291
+ # intact.
276
292
  #
277
293
  # @param paths [Enumerable<String>]
278
- # @return [Array<Finding>] one per unreachable comment-form annotation,
279
- # in file-then-line order. A file that cannot be read contributes
294
+ # @return [Array<Finding>] one per unreachable annotation, in
295
+ # file-then-line order. A file that cannot be read contributes
280
296
  # nothing here — {ValidatorBackend} already reports read failures, and
281
297
  # this pass has nothing positional to say about a file it never saw.
282
298
  def unreachable_findings(paths)
@@ -300,6 +316,13 @@ module SpecGuard
300
316
  "so the one-line lookback claims only the line just above the example " \
301
317
  "and this annotation is silently discarded at extraction (SPGD-12 §2)"
302
318
 
319
+ UNREACHABLE_GROUP_ANNOTATION =
320
+ "unreachable annotation: this @intent: sits trailing on an example-group line " \
321
+ "(describe/context/...), and annotations attach to examples, never to groups — " \
322
+ "extraction (SPGD-12 §2) is one-line and example-anchored, so no example can ever " \
323
+ "claim it. Move it onto its example's `it` line, or to the comment line directly " \
324
+ "above that `it`"
325
+
303
326
  # A comment-only line carrying an `@intent:` token — the only form an
304
327
  # example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
305
328
  COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
@@ -312,11 +335,32 @@ module SpecGuard
312
335
  # Anchoring on the example is what keeps this pass off annotation
313
336
  # corpora with no examples at all (the recorded-binary fixtures in
314
337
  # spec/fixtures/): there is no lookback there to silently discard
315
- # anything, so there is nothing to report. The defect this pass exists
316
- # for (SPGD-897) is stacked lines above a real example — an annotation
317
- # the author believed was attached to the `it` under it.
338
+ # anything, so there is nothing to report. The defects this pass exists
339
+ # for are annotations an author believed were attached to a real
340
+ # example: stacked lines above it (SPGD-897), and the trailing form on
341
+ # a group line directly above it (SPGD-1510).
318
342
  EXAMPLE_LINE = /\A\s*(?:it|specify)\b/
319
343
 
344
+ # @param text [String] source of one file
345
+ # @param file [String] path to record on each Finding
346
+ # @return [Array<Finding>] the structural findings for one file, in line
347
+ # order: one per comment-form `@intent:` line in a stacked run (see
348
+ # {stacked_findings_in_text}) plus one per group line carrying the
349
+ # trailing form (see {group_line_findings_in_text}). The two passes
350
+ # are disjoint by construction — one reads only comment-only lines,
351
+ # the other only lines that are not — so the merge cannot double-flag
352
+ # a line, and the sort restores the file-then-line order
353
+ # {unreachable_findings} promises.
354
+ def unreachable_findings_in_text(text, file:)
355
+ # A file that is not valid UTF-8 is reported once, loudly, by the
356
+ # backend as a read failure; nothing positional can be said about it,
357
+ # and `lines` below would raise on the invalid bytes.
358
+ return [] unless text.valid_encoding?
359
+
360
+ (stacked_findings_in_text(text, file: file) +
361
+ group_line_findings_in_text(text, file: file)).sort_by(&:line)
362
+ end
363
+
320
364
  # @param text [String] source of one file
321
365
  # @param file [String] path to record on each Finding
322
366
  # @return [Array<Finding>] one per comment-form `@intent:` line in a
@@ -324,7 +368,7 @@ module SpecGuard
324
368
  # lines immediately above an example — except the run's LAST line,
325
369
  # which is the one the one-line lookback claims. For the canonical
326
370
  # two-line stack above one `it`, that is the UPPER line.
327
- def unreachable_findings_in_text(text, file:)
371
+ def stacked_findings_in_text(text, file:)
328
372
  # A file that is not valid UTF-8 is reported once, loudly, by the
329
373
  # backend as a read failure; nothing positional can be said about it,
330
374
  # and `lines` below would raise on the invalid bytes.
@@ -354,6 +398,83 @@ module SpecGuard
354
398
 
355
399
  findings
356
400
  end
401
+
402
+ # RSpec's example-group keywords, at the start of a line and optionally
403
+ # `RSpec.`-prefixed. A line matching this hosts a GROUP, not an example
404
+ # — and {AnnotationLookup}'s extraction can claim an annotation only
405
+ # from an example's own line or the comment-only line above it, so an
406
+ # `@intent:` written here is unreachable wherever it sits on the line.
407
+ GROUP_LINE =
408
+ /\A\s*(?:RSpec\.)?(?:describe|context|feature|shared_examples|shared_examples_for|shared_context|example_group)\b/
409
+
410
+ # A token WITH its payload opener: what the rest of the pipeline treats
411
+ # as an annotation on a group line. The `{` is load-bearing — a bare
412
+ # `@intent:` token is a {AnnotationScanner::NO_PAYLOAD} extraction
413
+ # failure the main pipeline already reports loudly, and (because
414
+ # extraction is marker-based, SPGD-8 §7) the token is ALSO found inside
415
+ # quoted strings: this repo's own suite carries
416
+ # `describe "unreachable stacked @intent: annotations"`, which is prose,
417
+ # not an annotation, and must not read as one. A line matching this is
418
+ # one the scanner captured a payload from (or began capturing one),
419
+ # which is the population this pass has standing to judge. The naive
420
+ # search inherits the scanner's string-literal limitation in the exotic
421
+ # direction only: a group line whose prose embeds `@intent: {` reads as
422
+ # annotated (a flag the pipeline independently agrees with, since
423
+ # extraction captures that literal too), never the reverse.
424
+ INTENT_WITH_PAYLOAD = /@intent:\s*\{/
425
+
426
+ # The one-liner exemption: a group line that ALSO defines an example on
427
+ # the same line is that example's own line, so its trailing annotation
428
+ # is claimed by the example and must not be flagged:
429
+ #
430
+ # describe "one" do it("x") { } end # @intent: { ... }
431
+ #
432
+ # Two guards keep prose from reading as an example call. The match runs
433
+ # against the code BEFORE the `@intent:` token — the payload is English
434
+ # an author wrote about a behavior ("...when it's given one"), and
435
+ # reading `it'` there as a call exempted the exact group-line shape
436
+ # this pass exists to flag. And the call must sit after a block opener
437
+ # (`do`, `{`, `;`), because the description string is part of the code
438
+ # side too: `describe "the cost of rendering it" do` ends in the word
439
+ # `it`, and `it"` is not a call. What this heuristic still cannot see:
440
+ # a description string containing the literal sequence `do it` /
441
+ # `do specify` (or `{ it`), as in `describe "how to do it right" do`,
442
+ # still reads as a call and is wrongly exempted — a missed flag, and
443
+ # only ever a missed flag: prose can turn the exemption ON, never a
444
+ # real example OFF. The one-line example's own payload is never
445
+ # consulted, so a payload can never exempt anything.
446
+ EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*(?:it|specify)\b/
447
+
448
+ # @param text [String] source of one file
449
+ # @param file [String] path to record on each Finding
450
+ # @return [Array<Finding>] one per example-group line carrying an
451
+ # `@intent:` token, for lines that define no example of their own
452
+ # ({EXAMPLE_ON_LINE}). Comment-only lines are left to
453
+ # {stacked_findings_in_text}: a comment above a group is a different
454
+ # shape, deliberately out of scope here (SPGD-1510 flags the group
455
+ # line ITSELF).
456
+ def group_line_findings_in_text(text, file:)
457
+ # Same UTF-8 contract as the stacked pass above.
458
+ return [] unless text.valid_encoding?
459
+
460
+ text.lines.each_with_index.filter_map do |line, idx|
461
+ # Comment-only lines belong to the stacked pass; this pass reads the
462
+ # group line ITSELF. (A `#`-leading line could never match
463
+ # GROUP_LINE anyway — the guard states the division, not a filter.)
464
+ next if line.lstrip.start_with?("#")
465
+ next unless GROUP_LINE.match?(line)
466
+ next unless INTENT_WITH_PAYLOAD.match?(line)
467
+ # The exemption sees the code before the token; the payload and
468
+ # anything after it are prose and must never read as an example
469
+ # call. The token is guaranteed present (INTENT_WITH_PAYLOAD just
470
+ # matched), so `split` always yields the code side.
471
+ code = line.split("@intent:", 2).first
472
+ next if EXAMPLE_ON_LINE.match?(code)
473
+
474
+ Finding.new(file: file, line: idx + 1, problem: UNREACHABLE_GROUP_ANNOTATION,
475
+ kind: Finding::KIND_UNREACHABLE)
476
+ end
477
+ end
357
478
  end
358
479
  end
359
480
  end
@@ -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.24"
10
+ VERSION = "0.3.26"
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.24
4
+ version: 0.3.26
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-24 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json