specguard-ruby 0.3.28 → 0.3.30

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: 58f8188803b68968be3289f01834d0f128f239b10d31162d34eeaaf55b6daefe
4
- data.tar.gz: 273ce5b5eb675b12b302e23c3b24e025d81e60e77b77476ba9c7401fd7742600
3
+ metadata.gz: 916a7326516ee737b20d0a0790fd6b6a0f1abe56404083ad67fe61d7d7fb5c46
4
+ data.tar.gz: 0b39ff94f46fb053ea659cb5f69f878bf86a3b04ed1e0e630394769bed0d6589
5
5
  SHA512:
6
- metadata.gz: fcc5a85ecf595eeb62aae5446387ff74802579f97991c71b9b7a61014499c954d6fcc546171440dd3fde330b06ad11cc6fb2717b5f8f569aee884d51534c1f61
7
- data.tar.gz: 52ed4889c3074cb474b9a192603414ba11644c753ad6dc2a3d946579663bf0432bcd533702f36568d9c81259a4eaf3e7735102a5374379fbabe0a6b4f24b76e5
6
+ metadata.gz: 0d721e5d2d50648b3dfc7cf22ebbd2af16a9edd422feffa80b6d32d1092860c145ef6598919a802e3341a3eaa9fea6341fa26cab71f8abef33e0ff9d74d127b6
7
+ data.tar.gz: 77a24a2798088706388f6ef2ba7c6d4bd648d4fd152b1dcf55eb9e391409a0ece0ec3046d6b0b14bfaa24db03e68dac7e89d4eef340d6c34ba6b9512acdfda7a
data/README.md CHANGED
@@ -44,13 +44,27 @@ be assumed, require it explicitly before the run:
44
44
  bundle exec ruby -rminitest/specguard_plugin -e 'Minitest.extensions << "specguard"; load ARGV[0]' test/your_suite.rb
45
45
  ```
46
46
 
47
+ Configure it from Ruby, if the environment is not the right place, through the framework-neutral
48
+ `SpecGuard.configure` — in a `test_helper.rb`, before the run starts:
49
+
50
+ ```ruby
51
+ SpecGuard.configure do |config|
52
+ config.branch = "release/2.0"
53
+ end
54
+ ```
55
+
56
+ `SpecGuard::RSpec.configure` is the same call under its RSpec-era spelling: both return the one
57
+ configuration object the reporter and the formatter read, so either works from either framework.
58
+ See [Shipping the run to SpecGuard](#shipping-the-run-to-specguard) for the settings.
59
+
47
60
  ## The linter — `specguard-lint`
48
61
 
49
62
  Validates `# @intent:` annotations in changed (or all) `*_spec.rb` files against the OpenTestIntent
50
63
  JSON Schema. Exits `1` on a malformed annotation — or a well-formed but unreachable one (stacked
51
64
  above another comment-form `@intent:` line, so the one-line lookback never claims it; separated
52
- from its `it` by one blank or ordinary comment line, which puts it out of the lookback's reach; or
53
- trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
65
+ from its example (`it`, `specify`, `xit`, `fit`, `example`, …) by one blank or ordinary comment line, which puts it out of the lookback's reach;
66
+ directly above an example that carries its own trailing `@intent:`, which extraction prefers, so the
67
+ comment is shadowed; or trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
54
68
  **never** fails on a *missing* one (adoption is opt-in and gradual).
55
69
 
56
70
  ```bash
@@ -336,8 +350,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
336
350
  is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
337
351
  my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
338
352
  but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
339
- never claims it, separated from its `it` by one blank or ordinary comment line, or trailing on a
340
- `describe`/`context` group line, which no example's extraction can ever reach — and nothing else.
353
+ never claims it, separated from its example by one blank or ordinary comment line, shadowed by its
354
+ example's own trailing `@intent:`, or trailing on a `describe`/`context` group line, which no example's extraction can ever reach — and nothing else.
341
355
 
342
356
  ## The formatter — `SpecGuard::RSpecFormatter`
343
357
 
@@ -440,8 +454,9 @@ the one on the next line.
440
454
  # SPECGUARD_SHARD_ID / SPECGUARD_OUTPUT_PATH / SPECGUARD_LOCAL_OUTPUT_PATH
441
455
  # override any of it.
442
456
  #
443
- # Assign a value here only when it is one neither source can know:
444
- SpecGuard::RSpec.configure do |config|
457
+ # Assign a value here only when it is one neither source can know
458
+ # (`SpecGuard::RSpec.configure` is the same call, under its RSpec-era name):
459
+ SpecGuard.configure do |config|
445
460
  config.branch = "release/2.0"
446
461
  end
447
462
  ```
@@ -461,7 +476,7 @@ export SPECGUARD_TIMEOUT=10 # optional; seconds, applied to connect and
461
476
 
462
477
  ```ruby
463
478
  # ...or in Ruby, if you would rather not use the environment
464
- SpecGuard::RSpec.configure do |config|
479
+ SpecGuard.configure do |config|
465
480
  config.endpoint = "https://specguard.example.com"
466
481
  config.api_key = ENV["SPECGUARD_API_KEY"]
467
482
  config.timeout = 10
@@ -473,7 +488,7 @@ is written to `log/test_results.local.jsonl` — the local development record,
473
488
  kept apart from the replay queue — so local development needs no opt-out, and a
474
489
  fork with no secret configured behaves like a laptop rather than like a broken
475
490
  build. The local file's name is configurable via `SPECGUARD_LOCAL_OUTPUT_PATH`
476
- (or `SpecGuard::RSpec.configure { |c| c.local_output_path = ... }`). The write
491
+ (or `SpecGuard.configure { |c| c.local_output_path = ... }`). The write
477
492
  itself is silent when it succeeds — the ordinary case on any machine that can
478
493
  create `log/` — and it usually does.
479
494
 
data/bin/specguard-ingest CHANGED
@@ -9,13 +9,13 @@
9
9
  # the suite already finished, instead of re-running the suite.
10
10
  #
11
11
  # Exit codes are the contract, and they are `specguard-lint`'s reasoning
12
- # transferred (see SpecGuard::RSpec::IngestCLI):
12
+ # transferred (see SpecGuard::Client::IngestCLI):
13
13
  #
14
14
  # 0 every line was accepted
15
15
  # 1 at least one line was refused by the endpoint
16
16
  # 2 this tool could not do its job — misuse, or the tool itself broken
17
17
  #
18
- # SpecGuard::RSpec::IngestCLI#run is written to *return* one of those three and
18
+ # SpecGuard::Client::IngestCLI#run is written to *return* one of those three and
19
19
  # never to raise. The `require`s below are the one thing outside its reach, so
20
20
  # they get the same treatment here: a gem that cannot even load is the tool
21
21
  # being broken, which is a 2. Left bare, Ruby would exit 1 and report that the
@@ -23,18 +23,18 @@
23
23
 
24
24
  # Allow running from a source checkout without bundler by putting the local
25
25
  # lib/ on the load path. When the gem is installed (or run under
26
- # `bundle exec`), specguard/rspec resolves through the normal load path.
26
+ # `bundle exec`), specguard/client resolves through the normal load path.
27
27
  source_lib = File.expand_path("../lib", __dir__)
28
28
  $LOAD_PATH.unshift(source_lib) unless $LOAD_PATH.include?(source_lib)
29
29
 
30
30
  begin
31
- require "specguard/rspec"
31
+ require "specguard/client"
32
32
  # By its own path, deliberately. `net/http`, `uri` and `zlib` are not on the
33
33
  # linter's load chain and this command is the reason to pay for them.
34
- require "specguard/rspec/ingest_cli"
34
+ require "specguard/client/ingest_cli"
35
35
  rescue ScriptError, StandardError => e
36
36
  warn "specguard-ingest: error: could not load specguard-ruby: #{e.class}: #{e.message}"
37
37
  exit 2
38
38
  end
39
39
 
40
- exit SpecGuard::RSpec::IngestCLI.new.run(ARGV)
40
+ exit SpecGuard::Client::IngestCLI.new.run(ARGV)
data/bin/specguard-lint CHANGED
@@ -11,7 +11,7 @@
11
11
  # 1 one or more annotations are malformed
12
12
  # 2 the linter could not do its job — misuse, or the tool itself broken
13
13
  #
14
- # SpecGuard::RSpec::CLI#run is written to *return* one of those three and never
14
+ # SpecGuard::Client::CLI#run is written to *return* one of those three and never
15
15
  # to raise; see its class comment for why that takes deliberate effort in Ruby.
16
16
  # The `require` below is the one thing outside its reach, so it gets the same
17
17
  # treatment here: a gem that cannot even load is the linter being broken, which
@@ -20,15 +20,15 @@
20
20
 
21
21
  # Allow running from a source checkout without bundler by putting the local
22
22
  # lib/ on the load path. When the gem is installed (or run under
23
- # `bundle exec`), specguard/rspec resolves through the normal load path.
23
+ # `bundle exec`), specguard/client resolves through the normal load path.
24
24
  source_lib = File.expand_path("../lib", __dir__)
25
25
  $LOAD_PATH.unshift(source_lib) unless $LOAD_PATH.include?(source_lib)
26
26
 
27
27
  begin
28
- require "specguard/rspec"
28
+ require "specguard/client"
29
29
  rescue ScriptError, StandardError => e
30
30
  warn "specguard-lint: error: could not load specguard-ruby: #{e.class}: #{e.message}"
31
31
  exit 2
32
32
  end
33
33
 
34
- exit SpecGuard::RSpec::CLI.new.run(ARGV)
34
+ exit SpecGuard::Client::CLI.new.run(ARGV)
@@ -4,7 +4,7 @@
4
4
  # with what?*
5
5
  #
6
6
  # It is the formatter's half of the annotation story. The linter's half already
7
- # exists and is not duplicated here — {SpecGuard::RSpec::Scanner} finds every
7
+ # exists and is not duplicated here — {SpecGuard::Client::Scanner} finds every
8
8
  # `@intent:` in a file, captures its payload string-aware and parses it, and
9
9
  # the `validate-intent` binary decides whether the result is valid. This class
10
10
  # consumes both. Writing a second extractor would guarantee that the tool
@@ -12,14 +12,14 @@
12
12
  # platform eventually disagree about what an annotation *is*.
13
13
  #
14
14
  # Requiring the linter's chain from here is safe in the direction that matters:
15
- # `lib/specguard/rspec.rb` does not require `rspec/core`, so pulling it in from
15
+ # `lib/specguard/client.rb` does not require `rspec/core`, so pulling it in from
16
16
  # the formatter's side keeps `bin/specguard-lint` loadable on a machine with no
17
17
  # RSpec installed. spec/specguard/rspec/formatter_loading_spec.rb pins that in
18
18
  # both directions.
19
- require_relative "../rspec"
19
+ require_relative "../client"
20
20
 
21
21
  module SpecGuard
22
- module RSpec
22
+ module Client
23
23
  # == The lookback rule (SPGD-12 §2)
24
24
  #
25
25
  # `example.metadata[:line_number]` is the line the `it` is on. An annotation
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecGuard
4
- module RSpec
4
+ module Client
5
5
  # Finds `@intent:` annotations in test source and captures the object
6
6
  # literal that follows each one.
7
7
  #
@@ -3,7 +3,7 @@
3
3
  require "optparse"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # `specguard-lint`'s command line, and the whole of the exit contract.
8
8
  #
9
9
  # == The contract, and the reason it needs defending
@@ -126,9 +126,10 @@ 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/SPGD-1510/SPGD-1554: stacked above another
130
- # comment-form `@intent:` line so the one-line lookback never claims it,
131
- # separated from its example by one blank/comment line, or trailing on
129
+ # unreachable (SPGD-900/SPGD-1510/SPGD-1554/SPGD-1560: stacked above
130
+ # another comment-form `@intent:` line so the one-line lookback never
131
+ # claims it, separated from its example by one blank/comment line,
132
+ # shadowed by its example's own trailing `@intent:`, or trailing on
132
133
  # an example-group line no example's extraction can reach — a dead
133
134
  # contract in every case, and still a failure). The only code produced by
134
135
  # inspecting content, and the only path that reaches it is a failed
@@ -168,13 +169,15 @@ module SpecGuard
168
169
 
169
170
  results = backend.check(selection.files)
170
171
 
171
- # The structural pass (SPGD-900, SPGD-1510, SPGD-1554): annotations
172
+ # The structural pass (SPGD-900, SPGD-1510, SPGD-1554, SPGD-1560): annotations
172
173
  # that are valid in isolation but can never be extracted — stacked
173
174
  # consecutive comment-form `@intent:` lines, where the one-line
174
175
  # lookback (SPGD-12 §2) claims only the line just above the example,
175
176
  # comment-form runs separated from their example by one blank/comment
176
- # line, and `@intent:` tokens trailing on an example-group line, which
177
- # no extraction rule can ever reach. These are Linter::Results like any
177
+ # line, comment-form lines shadowed by their example's own trailing
178
+ # `@intent:` (extraction is own-line-first), and `@intent:` tokens
179
+ # trailing on an example-group line, which no extraction rule can ever
180
+ # reach. These are Linter::Results like any
178
181
  # other, so both renderers and the exit code pick them up with no
179
182
  # second path to keep in step.
180
183
  results += unreachable_results(selection.files)
@@ -355,7 +358,7 @@ module SpecGuard
355
358
  end
356
359
 
357
360
  # The fence arm of the `:all` empty reason: every matching file the walk
358
- # found was inside a {SpecGuard::RSpec::FileSelector::SKIPPED_DIRECTORIES}
361
+ # found was inside a {SpecGuard::Client::FileSelector::SKIPPED_DIRECTORIES}
359
362
  # directory, so the silence is the fence's, not the tree's.
360
363
  def all_fenced_reason(selection)
361
364
  n = selection.skipped
@@ -482,15 +485,15 @@ module SpecGuard
482
485
  #
483
486
  # The boundary is the set-difference the partition above already
484
487
  # supports: a file counts as covered when it yielded any line-scoped
485
- # result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554
488
+ # result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554/SPGD-1560
486
489
  # unreachable findings are line-scoped too and require an annotation to
487
490
  # exist, so they cover their file) or when it could not be read at all
488
491
  # (KIND_READ → `unread`). An unread file is NOT a zero-annotation file:
489
492
  # it already gets its own summary clause and FAIL line, and calling it
490
493
  # annotation-free would be the same overstatement `summary_line` refuses
491
494
  # to make. Unreachable findings need an annotation to exist — a stacked
492
- # comment-form pair, a separated comment-form run, or an `@intent:`
493
- # token on a group line — impossible in a file with none, so `annotations ∪ unread` already covers every
495
+ # comment-form pair, a separated comment-form run, a shadowed comment-form
496
+ # run, or an `@intent:` token on a group line — impossible in a file with none, so `annotations ∪ unread` already covers every
494
497
  # non-bare file and no third subtraction is needed.
495
498
  #
496
499
  # It is a NOTE on stderr, never a warning and never an exit code: "lint,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecGuard
4
- module RSpec
4
+ module Client
5
5
  # Where the checkout is sitting — which commit, and which branch — asked of
6
6
  # git directly.
7
7
  #
@@ -120,7 +120,7 @@ module SpecGuard
120
120
  # The escape hatch stays open on top of all of it, for a value neither
121
121
  # source can know:
122
122
  #
123
- # SpecGuard::RSpec.configure do |config|
123
+ # SpecGuard.configure do |config|
124
124
  # config.branch = "release/2.0"
125
125
  # end
126
126
  #
@@ -433,40 +433,5 @@ module SpecGuard
433
433
  parsed
434
434
  end
435
435
  end
436
-
437
- class << self
438
- # The process-wide formatter configuration.
439
- #
440
- # Built on first use rather than at load time so that a `configure` block
441
- # in a `spec_helper.rb` and a variable exported by a CI job describe the
442
- # same object regardless of which the interpreter reached first.
443
- #
444
- # @return [Configuration]
445
- def configuration
446
- @configuration ||= Configuration.new
447
- end
448
-
449
- # Configure the formatter.
450
- #
451
- # SpecGuard::RSpec.configure do |config|
452
- # config.branch = "release/2.0"
453
- # end
454
- #
455
- # @yieldparam configuration [Configuration]
456
- # @return [Configuration] the configuration, block or no block
457
- def configure
458
- yield configuration if block_given?
459
- configuration
460
- end
461
-
462
- # Drop the memoized configuration so the next read re-seeds from ENV.
463
- # Exists for tests, and for the rare caller that changes the environment
464
- # after this file was loaded.
465
- #
466
- # @return [void]
467
- def reset_configuration!
468
- @configuration = nil
469
- end
470
- end
471
436
  end
472
437
  end
@@ -3,7 +3,7 @@
3
3
  require "open3"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # Chooses which spec files the linter reads.
8
8
  #
9
9
  # Two modes: every Ruby test file under the working directory (the
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecGuard
4
- module RSpec
4
+ module Client
5
5
  # One `@intent:` annotation, located and (where possible) parsed.
6
6
  #
7
7
  # Exactly one of `intent` and `problem` is non-nil:
@@ -25,7 +25,7 @@ module SpecGuard
25
25
  #
26
26
  # NOTE: this is a subclass of an anonymous `Data.define` rather than a
27
27
  # `Data.define do ... end` block, because a constant assigned inside that
28
- # block binds to the *lexically* enclosing module (SpecGuard::RSpec) rather
28
+ # block binds to the *lexically* enclosing module (SpecGuard::Client) rather
29
29
  # than to the value class — so KIND_EXTRACTION below would silently not be
30
30
  # `Finding::KIND_EXTRACTION`.
31
31
  class Finding < Data.define(:file, :line, :intent, :problem, :kind)
@@ -49,7 +49,7 @@ module SpecGuard
49
49
  KIND_SCHEMA = :schema
50
50
 
51
51
  # The annotation is well-formed in isolation but can never be EXTRACTED.
52
- # Three shapes, all dead the moment they are written:
52
+ # Four shapes, all dead the moment they are written:
53
53
  #
54
54
  # * a comment-form `@intent:` stacked above another comment-form
55
55
  # `@intent:` line, so the one-line lookback (SPGD-12 §2 —
@@ -61,7 +61,10 @@ module SpecGuard
61
61
  # it, and the example beneath it silently ingests unannotated;
62
62
  # * a comment-form `@intent:` run separated from its example by ONE
63
63
  # blank or ordinary comment line (SPGD-1554): the one-line lookback
64
- # reads only that interleave, so the whole run is out of reach.
64
+ # reads only that interleave, so the whole run is out of reach;
65
+ # * a comment-form `@intent:` directly above an example whose OWN line
66
+ # also carries a trailing `@intent:` (SPGD-1560): extraction is
67
+ # own-line-first, so the example's own annotation shadows the comment.
65
68
  #
66
69
  # Whichever shape, the contract is dead metadata: counted by the linter,
67
70
  # discarded by extraction. Produced by {Scanner}'s structural pass, not
@@ -3,21 +3,21 @@
3
3
  require "json"
4
4
  require "optparse"
5
5
 
6
- require_relative "../rspec"
6
+ require_relative "../client"
7
7
  require_relative "ingest_reporter"
8
8
  require_relative "transport"
9
9
 
10
10
  # `specguard-ingest`'s command line — the other end of `log/test_results.jsonl`.
11
11
  #
12
- # == Why this file is not on `require "specguard/rspec"`'s chain
12
+ # == Why this file is not on `require "specguard/client"`'s chain
13
13
  #
14
14
  # `specguard-lint` runs on machines that never make a network call, and putting
15
15
  # this file on the umbrella's chain would put `net/http`, `uri` and `zlib` on
16
16
  # the linter's load path to serve a command it never invokes. So it is loaded by
17
17
  # its own path, exactly as the formatter is and for the same reason —
18
- # `require "specguard/rspec/ingest_cli"`, which `bin/specguard-ingest` does.
18
+ # `require "specguard/client/ingest_cli"`, which `bin/specguard-ingest` does.
19
19
  module SpecGuard
20
- module RSpec
20
+ module Client
21
21
  # Replays a saved run: reads a `log/test_results.jsonl` back and re-delivers
22
22
  # each line through the {Transport} this gem already ships.
23
23
  #
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # `specguard-ingest --json`: the machine-readable renderer over the same
8
8
  # per-line facts the human report is built from.
9
9
  #
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # `specguard-lint --json`: the machine-readable renderer over the same
8
8
  # `Array<Linter::Result>` the text report is built from.
9
9
  #
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # The shared verdict shape both renderers and the exit code derive from.
8
8
  #
9
9
  # == Where this sits after SPGD-867 (the cutover)
@@ -40,15 +40,17 @@ module SpecGuard
40
40
  # `Finding::KIND_UNREACHABLE` — an annotation that is well-formed in
41
41
  # isolation but can never be extracted, so the one-line lookback
42
42
  # (SPGD-12 §2) never claims it — is also reported as a failure, and
43
- # therefore also exits 1. Three shapes reach this kind: a comment-form
43
+ # therefore also exits 1. Four shapes reach this kind: a comment-form
44
44
  # `@intent:` stacked above another comment-form `@intent:` line (SPGD-900),
45
45
  # an `@intent:` trailing on an example-group line, which is no
46
46
  # example's own and not comment-only (SPGD-1510), and a comment-form
47
47
  # `@intent:` run separated from its example by one blank or ordinary
48
- # comment line (SPGD-1554). Nothing about any of them is malformed; the
48
+ # comment line (SPGD-1554), and a comment-form `@intent:` directly above an
49
+ # example whose own line carries a trailing `@intent:`, which own-line-first
50
+ # extraction prefers (SPGD-1560). Nothing about any of them is malformed; the
49
51
  # contract is dead metadata, counted by the linter and discarded by
50
52
  # extraction, and the structural pass that flags them (SPGD-900,
51
- # SPGD-1510, SPGD-1554) reports it loudly rather than letting it pass as
53
+ # SPGD-1510, SPGD-1554, SPGD-1560) reports it loudly rather than letting it pass as
52
54
  # clean.
53
55
  module Linter
54
56
  # One annotation's verdict. `problem` is set when discovery could not
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # Relaxes PROTOCOL.md §1's permissive annotation syntax into strict JSON.
8
8
  #
9
9
  # §1 promises "the linter normalizes before validating" and lists three
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  module SpecGuard
6
- module RSpec
6
+ module Client
7
7
  # Runs the discovery pipeline over source files and returns {Finding}s.
8
8
  #
9
9
  # Pipeline, per `@intent:` token:
@@ -261,7 +261,7 @@ module SpecGuard
261
261
  # ({AnnotationLookup}, SPGD-12 §2) is positional: an annotation reaches
262
262
  # an example only through its own line (the trailing form, and only when
263
263
  # that line IS the example's) or through the comment-form line
264
- # IMMEDIATELY ABOVE it. Three shapes fall outside both and are dead the
264
+ # IMMEDIATELY ABOVE it. Four shapes fall outside both and are dead the
265
265
  # moment they are written — silently discarded by extraction while this
266
266
  # pipeline counts them as valid annotations and the run exits 0:
267
267
  #
@@ -283,7 +283,15 @@ module SpecGuard
283
283
  # the example, which is the interleave — so the annotation is out of
284
284
  # reach and every line of the run is dead.
285
285
  #
286
- # All three are reported loudly, not skipped — the same stance
286
+ # * the SHADOWED annotation (SPGD-1560, ported from specguard-ts
287
+ # SPGD-1556): a comment-form `@intent:` run directly above an example
288
+ # whose OWN line also carries a trailing `@intent: {`. Extraction is
289
+ # own-line-first ({AnnotationLookup::Index#intent_for} returns the
290
+ # example's own annotation before it ever consults the line above),
291
+ # so the comment is shadowed and its LAST line is dead. (Any earlier
292
+ # lines of the run are the stacked arm's.)
293
+ #
294
+ # All four are reported loudly, not skipped — the same stance
287
295
  # {AnnotationScanner} takes for NO_PAYLOAD.
288
296
  #
289
297
  # A comment-form annotation line is a comment-only line
@@ -336,10 +344,28 @@ module SpecGuard
336
344
  "discarded at extraction. Move it directly above the example (or merge it into " \
337
345
  "the line that is)"
338
346
 
347
+ UNREACHABLE_SHADOWED_ANNOTATION =
348
+ "unreachable annotation: the example directly below carries its own trailing @intent:, " \
349
+ "and extraction is own-line-first (SPGD-12 §2), so this comment-form annotation is " \
350
+ "shadowed and silently discarded. Delete this comment, or merge it into the trailing " \
351
+ "payload"
352
+
339
353
  # A comment-only line carrying an `@intent:` token — the only form an
340
354
  # example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
341
355
  COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
342
356
 
357
+ # The one source of the example-call vocabulary, shared by
358
+ # {EXAMPLE_LINE} and {EXAMPLE_ON_LINE}. `it`/`specify` are matched on
359
+ # `\b` alone; the other eight keywords also carry a `(?![:.])`
360
+ # lookahead, which is load-bearing: `example.run` (the around-hook
361
+ # idiom), `example.metadata[:x]` and `focus: true` are not example
362
+ # definitions and must never anchor. (`\b` already excludes `fitness`
363
+ # and `example_group`, a GROUP_LINE keyword.) `skip` and `pending` are
364
+ # deliberately EXCLUDED: at line start they are the runtime call inside
365
+ # an example body, not an example definition.
366
+ EXAMPLE_CALL_SOURCE =
367
+ "(?:(?:it|specify)\\b|(?:example|focus|fexample|fit|fspecify|xexample|xit|xspecify)\\b(?![:.]))"
368
+
343
369
  # The line an annotation run may be claimed from: the first example
344
370
  # keyword. The lookback rule is `example.metadata[:line_number] - 1`, so
345
371
  # only the comment form on the line immediately above an example is
@@ -353,7 +379,14 @@ module SpecGuard
353
379
  # example: stacked lines above it (SPGD-897), the trailing form on
354
380
  # a group line directly above it (SPGD-1510), and a comment-form run
355
381
  # separated from it by one blank/comment line (SPGD-1554).
356
- EXAMPLE_LINE = /\A\s*(?:it|specify)\b/
382
+ #
383
+ # The vocabulary is every example method RSpec core defines
384
+ # (`example focus fexample fit fspecify xexample xit xspecify`, besides
385
+ # `it`/`specify`): lookup is keyed by `example.metadata[:line_number]`, so
386
+ # an annotation stacked/separated/shadowed above `xit` is exactly as dead
387
+ # as one above `it`. Both this anchor and {EXAMPLE_ON_LINE} are built
388
+ # from {EXAMPLE_CALL_SOURCE}, so anchor and exemption cannot drift.
389
+ EXAMPLE_LINE = /\A\s*#{EXAMPLE_CALL_SOURCE}/
357
390
 
358
391
  # A blank (whitespace-only) line — one of the two interleaves the
359
392
  # separated pass reads. `\n` is whitespace, so a `text.lines` element
@@ -370,14 +403,18 @@ module SpecGuard
370
403
  # @return [Array<Finding>] the structural findings for one file, in line
371
404
  # order: one per comment-form `@intent:` line in a stacked run (see
372
405
  # {stacked_findings_in_text}), one per line of a separated run (see
373
- # {separated_findings_in_text}) plus one per group line carrying the
374
- # trailing form (see {group_line_findings_in_text}). The three passes
375
- # are disjoint by construction: the stacked pass needs an example
376
- # line directly after the run, the separated pass needs a blank or
377
- # comment-only line there (one slot cannot be both), and the group
378
- # pass reads only lines that are not comment-only — so the merge
379
- # cannot double-flag a line, and the sort restores the file-then-line
380
- # order {unreachable_findings} promises.
406
+ # {separated_findings_in_text}), one per run whose example carries its own
407
+ # trailing annotation (see {own_line_shadow_findings_in_text}) plus one
408
+ # per group line carrying the trailing form (see
409
+ # {group_line_findings_in_text}). The four passes are disjoint by
410
+ # construction: the stacked pass flags all but the LAST line of a run
411
+ # when an example directly follows, the shadow pass flags ONLY that last
412
+ # line (and only when the example also carries `@intent: {`), the
413
+ # separated pass needs a blank or comment-only line after the run (one
414
+ # slot cannot be both that and an example), and the group pass reads
415
+ # only lines that are not comment-only — so the merge cannot double-flag
416
+ # a line, and the sort restores the file-then-line order
417
+ # {unreachable_findings} promises.
381
418
  def unreachable_findings_in_text(text, file:)
382
419
  # A file that is not valid UTF-8 is reported once, loudly, by the
383
420
  # backend as a read failure; nothing positional can be said about it,
@@ -386,6 +423,7 @@ module SpecGuard
386
423
 
387
424
  (stacked_findings_in_text(text, file: file) +
388
425
  separated_findings_in_text(text, file: file) +
426
+ own_line_shadow_findings_in_text(text, file: file) +
389
427
  group_line_findings_in_text(text, file: file)).sort_by(&:line)
390
428
  end
391
429
 
@@ -473,6 +511,44 @@ module SpecGuard
473
511
  findings
474
512
  end
475
513
 
514
+ # @param text [String] source of one file
515
+ # @param file [String] path to record on each Finding
516
+ # @return [Array<Finding>] one per maximal run of comment-form
517
+ # `@intent:` lines whose following line is an example
518
+ # ({EXAMPLE_LINE}) that ALSO carries a trailing `@intent: {`
519
+ # ({INTENT_WITH_PAYLOAD}) — reported on the run's LAST line, the one
520
+ # the lookback would otherwise claim. Extraction is own-line-first
521
+ # (SPGD-12 §2), so the example's own annotation always wins and that
522
+ # comment is dead. Earlier lines of a longer run are the stacked arm's
523
+ # ({stacked_findings_in_text}); each dead line is flagged exactly once.
524
+ def own_line_shadow_findings_in_text(text, file:)
525
+ # Same UTF-8 contract as the stacked pass above.
526
+ return [] unless text.valid_encoding?
527
+
528
+ lines = text.lines
529
+ i = 0
530
+ findings = []
531
+
532
+ while i < lines.length
533
+ unless COMMENT_INTENT_LINE.match?(lines[i])
534
+ i += 1
535
+ next
536
+ end
537
+
538
+ i += 1 while i < lines.length && COMMENT_INTENT_LINE.match?(lines[i])
539
+ run_end = i # exclusive; lines[run_end] is the line after the run
540
+
541
+ example = lines[run_end]
542
+ next if example.nil?
543
+ next unless EXAMPLE_LINE.match?(example) && INTENT_WITH_PAYLOAD.match?(example)
544
+
545
+ findings << Finding.new(file: file, line: run_end, problem: UNREACHABLE_SHADOWED_ANNOTATION,
546
+ kind: Finding::KIND_UNREACHABLE)
547
+ end
548
+
549
+ findings
550
+ end
551
+
476
552
  # RSpec's example-group keywords, at the start of a line and optionally
477
553
  # `RSpec.`-prefixed. A line matching this hosts a GROUP, not an example
478
554
  # — and {AnnotationLookup}'s extraction can claim an annotation only
@@ -517,7 +593,7 @@ module SpecGuard
517
593
  # only ever a missed flag: prose can turn the exemption ON, never a
518
594
  # real example OFF. The one-line example's own payload is never
519
595
  # consulted, so a payload can never exempt anything.
520
- EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*(?:it|specify)\b/
596
+ EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*#{EXAMPLE_CALL_SOURCE}/
521
597
 
522
598
  # @param text [String] source of one file
523
599
  # @param file [String] path to record on each Finding
@@ -9,7 +9,7 @@ require_relative "configuration"
9
9
  require_relative "../version"
10
10
 
11
11
  module SpecGuard
12
- module RSpec
12
+ module Client
13
13
  # The one HTTP call this gem makes: `POST <endpoint>/api/v1/ingest`,
14
14
  # carrying a whole run.
15
15
  #
@@ -7,7 +7,7 @@ require "net/http"
7
7
  require "open3"
8
8
 
9
9
  module SpecGuard
10
- module RSpec
10
+ module Client
11
11
  # The Go validator backend: `validate-intent --source --json`, and since
12
12
  # SPGD-867 the ONLY validator `specguard-lint` has.
13
13
  #
@@ -36,7 +36,7 @@ module SpecGuard
36
36
  # consumers are `Formatter` and `Transport`, and `CLI` does not reference
37
37
  # it at all — so threading the lint backend through it would make the
38
38
  # linter depend on the telemetry object solely to read one env var. It also
39
- # memoizes process-wide (`SpecGuard::RSpec.configuration`), with
39
+ # memoizes process-wide (`SpecGuard.configuration`), with
40
40
  # `reset_configuration!` existing only for tests, which is a poor fit for
41
41
  # something a spec wants to vary per example. {CLI} takes an `env:` instead
42
42
  # and asks this module, so the seam is one hash away in a test and one
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+
5
+ module SpecGuard
6
+ module Client
7
+ # The Ruby client for SpecGuard.
8
+ #
9
+ # The linter is complete end to end: it finds `@intent:` annotations in
10
+ # spec files, captures each payload string-aware, normalizes PROTOCOL.md
11
+ # §1's permissive syntax into strict JSON, validates the result against the
12
+ # vendored OpenTestIntent schema, and reports violations in the reference
13
+ # tool's own grammar under the 0/1/2 exit contract (see {CLI}).
14
+ #
15
+ # Everything under `SpecGuard::Client` is framework-free: the RSpec
16
+ # formatter and the Minitest reporter are the framework halves of the
17
+ # client and both build on it. Neither is required from here — `rspec`
18
+ # is a development dependency of this gem, not a runtime one, and
19
+ # `bin/specguard-lint` loads this file on machines that may have no RSpec
20
+ # at all. Requiring `rspec/core` from this chain would turn a missing test
21
+ # framework into a broken linter. Load the formatter by its own path when
22
+ # you want it — `require "specguard/rspec/formatter"` — and see that file
23
+ # for the opt-in wiring.
24
+ class Error < StandardError; end
25
+
26
+ # A source line could not be scanned (unterminated string or object
27
+ # literal). Internal to the scanner: it is caught and turned into a
28
+ # {Finding}'s `problem` rather than reaching a caller.
29
+ class ScanError < Error; end
30
+
31
+ # The tool was invoked in a way that cannot work — `--changed` outside a git
32
+ # repository, say. Distinct from "the annotations are bad": the exit-code
33
+ # contract makes this a 2, and it is typed here so the CLI can map it
34
+ # without re-deriving the distinction from an error message.
35
+ class UsageError < Error; end
36
+
37
+ # The Go validator backend could not produce a verdict — the binary named
38
+ # by `SPECGUARD_VALIDATE_INTENT`, or the default binary the backend
39
+ # resolves when that is blank, is missing, will not execute, exited with a
40
+ # code that is not a verdict, or emitted something this cannot read as a
41
+ # report.
42
+ #
43
+ # Typed separately from {UsageError} because the two are different
44
+ # accusations ("you invoked me wrongly" vs "the tool I was told to use is
45
+ # broken"), and rescued *beside* it in {CLI#run} because the answer to both
46
+ # is the same and non-negotiable: exit 2, never 1. See {ValidatorBackend}.
47
+ class ValidatorError < Error; end
48
+
49
+ # The vendored canonical OpenTestIntent schema, copied byte-for-byte from
50
+ # open-test-intent so this gem has **no cross-repo runtime dependency**.
51
+ # It ships packaged (it lives under `lib/`, which the gemspec includes);
52
+ # the spec fixtures deliberately do not, so nothing here may be
53
+ # load-bearing at runtime.
54
+ #
55
+ # Since the SPGD-867 cutover nothing VALIDATES against this copy — every
56
+ # verdict comes from the `validate-intent` binary's own compiled-in
57
+ # schema. It survives because {ValidatorBackend}'s schema-contract check
58
+ # digests it at runtime and refuses a binary that would enforce different
59
+ # bytes: the two halves of the seam must keep meeting somewhere, and this
60
+ # file is where the gem's half lives.
61
+ SCHEMA_PATH = File.expand_path("client/schemas/open-test-intent.v1.json", __dir__).freeze
62
+ end
63
+ end
64
+
65
+ module SpecGuard
66
+ class << self
67
+ # The process-wide client configuration — one object shared by the RSpec
68
+ # formatter, the Minitest reporter and the CLIs, whatever framework a
69
+ # consumer runs.
70
+ #
71
+ # Built on first use rather than at load time so that a `configure` block
72
+ # in a `spec_helper.rb` / `test_helper.rb` and a variable exported by a CI
73
+ # job describe the same object regardless of which the interpreter reached
74
+ # first.
75
+ #
76
+ # {SpecGuard::RSpec.configure} (the documented RSpec entry point) delegates
77
+ # here and returns this same object — there is exactly one configuration,
78
+ # not one per entry point.
79
+ #
80
+ # @return [Client::Configuration]
81
+ def configuration
82
+ # Loaded here, not at the top of this file: the linter's chain
83
+ # (`require "specguard/client"`) must stay free of the formatter's
84
+ # configuration/transport half — formatter_loading_spec pins it.
85
+ require_relative "client/configuration"
86
+ @configuration ||= Client::Configuration.new
87
+ end
88
+
89
+ # Configure the client.
90
+ #
91
+ # SpecGuard.configure do |config|
92
+ # config.branch = "release/2.0"
93
+ # end
94
+ #
95
+ # @yieldparam configuration [Client::Configuration]
96
+ # @return [Client::Configuration] the configuration, block or no block
97
+ def configure
98
+ yield configuration if block_given?
99
+ configuration
100
+ end
101
+
102
+ # Drop the memoized configuration so the next read re-seeds from ENV.
103
+ # Exists for tests, and for the rare caller that changes the environment
104
+ # after this file was loaded.
105
+ #
106
+ # @return [void]
107
+ def reset_configuration!
108
+ @configuration = nil
109
+ end
110
+ end
111
+ end
112
+
113
+ require_relative "client/finding"
114
+ require_relative "client/annotation_scanner"
115
+ require_relative "client/payload_normalizer"
116
+ require_relative "client/scanner"
117
+ require_relative "client/file_selector"
118
+ require_relative "client/linter"
119
+ require_relative "client/json_reporter"
120
+ require_relative "client/validator_backend"
121
+ require_relative "client/cli"
@@ -4,11 +4,11 @@ require "json"
4
4
  require "fileutils"
5
5
 
6
6
  # Configuration and Transport are the framework-free halves of the client
7
- # (env resolution, wire delivery, the never-raise contract); both live under
8
- # the rspec namespace because rspec shipped first, neither contains any RSpec.
9
- # Required here rather than assumed because nothing else loads them on a
10
- # minitest-only consumer's machine.
11
- require_relative "../rspec/configuration"
7
+ # (env resolution, wire delivery, the never-raise contract), under
8
+ # `SpecGuard::Client`. Required here rather than assumed because nothing else
9
+ # loads them on a minitest-only consumer's machine.
10
+ require_relative "../client"
11
+ require_relative "../client/configuration"
12
12
 
13
13
  # The annotation lookup is the third framework-free half: what counts as a
14
14
  # test's annotation is a property of the source line, not of the framework
@@ -17,20 +17,19 @@ require_relative "../rspec/configuration"
17
17
  # already warns that a second extractor guarantees scanner and reporter
18
18
  # eventually disagree about what an annotation is). Requiring it pulls the
19
19
  # linter's chain — the same direction-safe require the formatter itself makes;
20
- # `specguard/rspec` does not require `rspec/core`, so a minitest-only machine
20
+ # `specguard/client` does not require `rspec/core`, so a minitest-only machine
21
21
  # stays loadable.
22
- require_relative "../rspec/annotation_lookup"
22
+ require_relative "../client/annotation_lookup"
23
23
 
24
24
  # The Minitest half of `specguard-ruby`. Everything the RSpec formatter knows
25
25
  # about the platform — the envelope's field names, the transport's
26
26
  # never-raise contract, the switch that a missing key is — is framework-free
27
- # knowledge that already lives in `SpecGuard::RSpec::Configuration` and
28
- # `SpecGuard::RSpec::Transport` (both named for the framework that happened to
29
- # ship first, neither containing any RSpec). This adapter contributes only the
27
+ # knowledge that already lives in `SpecGuard::Client::Configuration` and
28
+ # `SpecGuard::Client::Transport`. This adapter contributes only the
30
29
  # part that is genuinely Minitest's: turning `Minitest::Result` objects into
31
30
  # the same row shape the RSpec formatter emits, at the moment Minitest hands
32
31
  # them to a reporter — and attaching each test's annotation the same way the
33
- # formatter does, through the shared {SpecGuard::RSpec::AnnotationLookup} and
32
+ # formatter does, through the shared {SpecGuard::Client::AnnotationLookup} and
34
33
  # its one-line lookback (SPGD-12 §2), so a Minitest row carries the same
35
34
  # `status` / `intent` pair a RSpec row does.
36
35
  #
@@ -78,7 +77,7 @@ module SpecGuard
78
77
  end
79
78
  end
80
79
 
81
- def initialize(configuration: SpecGuard::RSpec.configuration,
80
+ def initialize(configuration: SpecGuard.configuration,
82
81
  transport: nil, output: $stderr,
83
82
  annotations: nil,
84
83
  clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
@@ -108,7 +107,7 @@ module SpecGuard
108
107
  # binds one root, hands it to the default lookup, and pins rspec-core's
109
108
  # lazily memoized relativization regex to the same moment.)
110
109
  root = self.class.repo_root
111
- @annotations = annotations || SpecGuard::RSpec::AnnotationLookup.new(root: root)
110
+ @annotations = annotations || SpecGuard::Client::AnnotationLookup.new(root: root)
112
111
  @clock = clock
113
112
  @rows = []
114
113
  @started_at = nil
@@ -244,8 +243,8 @@ module SpecGuard
244
243
  end
245
244
 
246
245
  def transport_for
247
- require_relative "../rspec/transport"
248
- SpecGuard::RSpec::Transport.new(
246
+ require_relative "../client/transport"
247
+ SpecGuard::Client::Transport.new(
249
248
  endpoint: @configuration.endpoint,
250
249
  api_key: @configuration.api_key,
251
250
  timeout: @configuration.timeout
@@ -40,13 +40,19 @@ require "rspec/core/formatters/base_formatter"
40
40
  require "json"
41
41
  require "fileutils"
42
42
 
43
- require_relative "configuration"
44
- require_relative "transport"
43
+ # `specguard/rspec` is what defines `SpecGuard::RSpec` — the namespace whose
44
+ # existence is the shadowing hazard documented below, and the home of the
45
+ # documented `SpecGuard::RSpec.configure` entry point. It is required here so
46
+ # that both stay true however this file is reached, now that the client chain
47
+ # no longer pulls it in.
48
+ require_relative "../rspec"
49
+ require_relative "../client/configuration"
50
+ require_relative "../client/transport"
45
51
  # Brings the linter's discovery chain with it (Scanner, Finding, Schema). That
46
52
  # direction is safe — `specguard/rspec` does not require `rspec/core`, so the
47
53
  # linter stays loadable without RSpec; it is only the reverse that would break
48
54
  # packaging.
49
- require_relative "annotation_lookup"
55
+ require_relative "../client/annotation_lookup"
50
56
 
51
57
  module SpecGuard
52
58
  # == A NOTE ON CONSTANT RESOLUTION — read this before editing
@@ -338,7 +344,7 @@ module SpecGuard
338
344
  root = self.class.repo_root
339
345
  ::RSpec::Core::Metadata.relative_path_regex
340
346
  @error_stream = error_stream
341
- @annotations = annotations || SpecGuard::RSpec::AnnotationLookup.new(root: root)
347
+ @annotations = annotations || SpecGuard::Client::AnnotationLookup.new(root: root)
342
348
  @specs = []
343
349
  @warned = false
344
350
  # Stamped here rather than from a `start` hook on purpose. `:start` is not
@@ -1031,7 +1037,7 @@ module SpecGuard
1031
1037
  end
1032
1038
 
1033
1039
  def transport_for(configuration)
1034
- SpecGuard::RSpec::Transport.new(
1040
+ SpecGuard::Client::Transport.new(
1035
1041
  endpoint: configuration.endpoint,
1036
1042
  api_key: configuration.api_key,
1037
1043
  timeout: configuration.timeout
@@ -1,72 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "rspec/version"
4
+ require_relative "client"
4
5
 
5
6
  module SpecGuard
7
+ # The RSpec-facing entry point of the gem.
8
+ #
9
+ # The framework-free client — scanner, linter, CLIs, configuration,
10
+ # transport — lives under {SpecGuard::Client}; the one component that is
11
+ # genuinely RSpec's is {SpecGuard::RSpecFormatter}
12
+ # (`require "specguard/rspec/formatter"`). What remains here is the
13
+ # documented `SpecGuard::RSpec.configure` entry point, kept working for
14
+ # anybody who configures the client from a `spec_helper.rb`.
15
+ #
16
+ # It is a thin delegate, not a second configuration: all three methods
17
+ # forward to {SpecGuard.configure} / {SpecGuard.configuration} /
18
+ # {SpecGuard.reset_configuration!}, so `SpecGuard::RSpec.configure` and
19
+ # `SpecGuard.configure` return the very same {Client::Configuration} object.
20
+ # A Minitest consumer has no reason to say `RSpec` and should configure
21
+ # through `SpecGuard.configure`.
6
22
  module RSpec
7
- # The Ruby client for SpecGuard.
8
- #
9
- # The linter is complete end to end: it finds `@intent:` annotations in
10
- # spec files, captures each payload string-aware, normalizes PROTOCOL.md
11
- # §1's permissive syntax into strict JSON, validates the result against the
12
- # vendored OpenTestIntent schema, and reports violations in the reference
13
- # tool's own grammar under the 0/1/2 exit contract (see {CLI}).
14
- #
15
- # The RSpec formatter (`SpecGuard::RSpecFormatter`) is the other half of
16
- # the client, and it is deliberately **not** required from here: `rspec` is
17
- # a development dependency of this gem, not a runtime one, and
18
- # `bin/specguard-lint` loads this file on machines that may have no RSpec
19
- # at all. Requiring `rspec/core` from this chain would turn a missing test
20
- # framework into a broken linter. Load it by its own path when you want it
21
- # — `require "specguard/rspec/formatter"` — and see that file for the
22
- # opt-in wiring.
23
- class Error < StandardError; end
23
+ class << self
24
+ # @yieldparam configuration [Client::Configuration]
25
+ # @return [Client::Configuration] the same object {SpecGuard.configure} returns
26
+ def configure(&)
27
+ SpecGuard.configure(&)
28
+ end
24
29
 
25
- # A source line could not be scanned (unterminated string or object
26
- # literal). Internal to the scanner: it is caught and turned into a
27
- # {Finding}'s `problem` rather than reaching a caller.
28
- class ScanError < Error; end
30
+ # @return [Client::Configuration]
31
+ def configuration
32
+ SpecGuard.configuration
33
+ end
29
34
 
30
- # The tool was invoked in a way that cannot work — `--changed` outside a git
31
- # repository, say. Distinct from "the annotations are bad": the exit-code
32
- # contract makes this a 2, and it is typed here so the CLI can map it
33
- # without re-deriving the distinction from an error message.
34
- class UsageError < Error; end
35
-
36
- # The Go validator backend could not produce a verdict — the binary named
37
- # by `SPECGUARD_VALIDATE_INTENT`, or the default binary the backend
38
- # resolves when that is blank, is missing, will not execute, exited with a
39
- # code that is not a verdict, or emitted something this cannot read as a
40
- # report.
41
- #
42
- # Typed separately from {UsageError} because the two are different
43
- # accusations ("you invoked me wrongly" vs "the tool I was told to use is
44
- # broken"), and rescued *beside* it in {CLI#run} because the answer to both
45
- # is the same and non-negotiable: exit 2, never 1. See {ValidatorBackend}.
46
- class ValidatorError < Error; end
47
-
48
- # The vendored canonical OpenTestIntent schema, copied byte-for-byte from
49
- # open-test-intent so this gem has **no cross-repo runtime dependency**.
50
- # It ships packaged (it lives under `lib/`, which the gemspec includes);
51
- # the spec fixtures deliberately do not, so nothing here may be
52
- # load-bearing at runtime.
53
- #
54
- # Since the SPGD-867 cutover nothing VALIDATES against this copy — every
55
- # verdict comes from the `validate-intent` binary's own compiled-in
56
- # schema. It survives because {ValidatorBackend}'s schema-contract check
57
- # digests it at runtime and refuses a binary that would enforce different
58
- # bytes: the two halves of the seam must keep meeting somewhere, and this
59
- # file is where the gem's half lives.
60
- SCHEMA_PATH = File.expand_path("rspec/schemas/open-test-intent.v1.json", __dir__).freeze
35
+ # @return [void]
36
+ def reset_configuration!
37
+ SpecGuard.reset_configuration!
38
+ end
39
+ end
61
40
  end
62
41
  end
63
-
64
- require_relative "rspec/finding"
65
- require_relative "rspec/annotation_scanner"
66
- require_relative "rspec/payload_normalizer"
67
- require_relative "rspec/scanner"
68
- require_relative "rspec/file_selector"
69
- require_relative "rspec/linter"
70
- require_relative "rspec/json_reporter"
71
- require_relative "rspec/validator_backend"
72
- require_relative "rspec/cli"
@@ -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.28"
10
+ VERSION = "0.3.30"
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.28
4
+ version: 0.3.30
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-30 00:00:00.000000000 Z
11
+ date: 2026-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -47,24 +47,25 @@ files:
47
47
  - bin/specguard-ingest
48
48
  - bin/specguard-lint
49
49
  - lib/minitest/specguard_plugin.rb
50
+ - lib/specguard/client.rb
51
+ - lib/specguard/client/annotation_lookup.rb
52
+ - lib/specguard/client/annotation_scanner.rb
53
+ - lib/specguard/client/cli.rb
54
+ - lib/specguard/client/configuration.rb
55
+ - lib/specguard/client/file_selector.rb
56
+ - lib/specguard/client/finding.rb
57
+ - lib/specguard/client/ingest_cli.rb
58
+ - lib/specguard/client/ingest_reporter.rb
59
+ - lib/specguard/client/json_reporter.rb
60
+ - lib/specguard/client/linter.rb
61
+ - lib/specguard/client/payload_normalizer.rb
62
+ - lib/specguard/client/scanner.rb
63
+ - lib/specguard/client/schemas/open-test-intent.v1.json
64
+ - lib/specguard/client/transport.rb
65
+ - lib/specguard/client/validator_backend.rb
50
66
  - lib/specguard/minitest/reporter.rb
51
67
  - lib/specguard/rspec.rb
52
- - lib/specguard/rspec/annotation_lookup.rb
53
- - lib/specguard/rspec/annotation_scanner.rb
54
- - lib/specguard/rspec/cli.rb
55
- - lib/specguard/rspec/configuration.rb
56
- - lib/specguard/rspec/file_selector.rb
57
- - lib/specguard/rspec/finding.rb
58
68
  - lib/specguard/rspec/formatter.rb
59
- - lib/specguard/rspec/ingest_cli.rb
60
- - lib/specguard/rspec/ingest_reporter.rb
61
- - lib/specguard/rspec/json_reporter.rb
62
- - lib/specguard/rspec/linter.rb
63
- - lib/specguard/rspec/payload_normalizer.rb
64
- - lib/specguard/rspec/scanner.rb
65
- - lib/specguard/rspec/schemas/open-test-intent.v1.json
66
- - lib/specguard/rspec/transport.rb
67
- - lib/specguard/rspec/validator_backend.rb
68
69
  - lib/specguard/rspec/version.rb
69
70
  - lib/specguard/version.rb
70
71
  - script/bump-version.sh