specguard-ruby 0.3.29 → 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: 887fc6d71732b1947cc48c34f27c68ac5b9b71c729f604cf95c41e508cc8d6af
4
- data.tar.gz: e2f20dbc68a7324cc34381e204bc8ac0f5db262002d86ffc3bd24c8c2cfb3663
3
+ metadata.gz: 916a7326516ee737b20d0a0790fd6b6a0f1abe56404083ad67fe61d7d7fb5c46
4
+ data.tar.gz: 0b39ff94f46fb053ea659cb5f69f878bf86a3b04ed1e0e630394769bed0d6589
5
5
  SHA512:
6
- metadata.gz: e3d18c2b2d19c8d25b0c5a9277847767996ff27be06f7f5802b052c7a4dab7f20e7244214a339fc0a4bc0343103572ad842bd381f95ccd6efa670422da445edc
7
- data.tar.gz: 9a2b14e04e71e6e957134e29ad38f11fdaad0aede17893907a048d9e6414d93affb7844f4a99e630194f5095c0bc945e271800871f39f71f325ed8b4805492ae
6
+ metadata.gz: 0d721e5d2d50648b3dfc7cf22ebbd2af16a9edd422feffa80b6d32d1092860c145ef6598919a802e3341a3eaa9fea6341fa26cab71f8abef33e0ff9d74d127b6
7
+ data.tar.gz: 77a24a2798088706388f6ef2ba7c6d4bd648d4fd152b1dcf55eb9e391409a0ece0ec3046d6b0b14bfaa24db03e68dac7e89d4eef340d6c34ba6b9512acdfda7a
data/README.md CHANGED
@@ -44,13 +44,26 @@ 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;
53
- directly above an `it` that carries its own trailing `@intent:`, which extraction prefers, so the
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
54
67
  comment is shadowed; or trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
55
68
  **never** fails on a *missing* one (adoption is opt-in and gradual).
56
69
 
@@ -337,8 +350,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
337
350
  is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
338
351
  my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
339
352
  but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
340
- never claims it, separated from its `it` by one blank or ordinary comment line, shadowed by its
341
- `it`'s own trailing `@intent:`, or trailing on a `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.
342
355
 
343
356
  ## The formatter — `SpecGuard::RSpecFormatter`
344
357
 
@@ -441,8 +454,9 @@ the one on the next line.
441
454
  # SPECGUARD_SHARD_ID / SPECGUARD_OUTPUT_PATH / SPECGUARD_LOCAL_OUTPUT_PATH
442
455
  # override any of it.
443
456
  #
444
- # Assign a value here only when it is one neither source can know:
445
- 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|
446
460
  config.branch = "release/2.0"
447
461
  end
448
462
  ```
@@ -462,7 +476,7 @@ export SPECGUARD_TIMEOUT=10 # optional; seconds, applied to connect and
462
476
 
463
477
  ```ruby
464
478
  # ...or in Ruby, if you would rather not use the environment
465
- SpecGuard::RSpec.configure do |config|
479
+ SpecGuard.configure do |config|
466
480
  config.endpoint = "https://specguard.example.com"
467
481
  config.api_key = ENV["SPECGUARD_API_KEY"]
468
482
  config.timeout = 10
@@ -474,7 +488,7 @@ is written to `log/test_results.local.jsonl` — the local development record,
474
488
  kept apart from the replay queue — so local development needs no opt-out, and a
475
489
  fork with no secret configured behaves like a laptop rather than like a broken
476
490
  build. The local file's name is configurable via `SPECGUARD_LOCAL_OUTPUT_PATH`
477
- (or `SpecGuard::RSpec.configure { |c| c.local_output_path = ... }`). The write
491
+ (or `SpecGuard.configure { |c| c.local_output_path = ... }`). The write
478
492
  itself is silent when it succeeds — the ordinary case on any machine that can
479
493
  create `log/` — and it usually does.
480
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
@@ -358,7 +358,7 @@ module SpecGuard
358
358
  end
359
359
 
360
360
  # The fence arm of the `:all` empty reason: every matching file the walk
361
- # found was inside a {SpecGuard::RSpec::FileSelector::SKIPPED_DIRECTORIES}
361
+ # found was inside a {SpecGuard::Client::FileSelector::SKIPPED_DIRECTORIES}
362
362
  # directory, so the silence is the fence's, not the tree's.
363
363
  def all_fenced_reason(selection)
364
364
  n = selection.skipped
@@ -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)
@@ -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)
@@ -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:
@@ -354,6 +354,18 @@ module SpecGuard
354
354
  # example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
355
355
  COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
356
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
+
357
369
  # The line an annotation run may be claimed from: the first example
358
370
  # keyword. The lookback rule is `example.metadata[:line_number] - 1`, so
359
371
  # only the comment form on the line immediately above an example is
@@ -367,7 +379,14 @@ module SpecGuard
367
379
  # example: stacked lines above it (SPGD-897), the trailing form on
368
380
  # a group line directly above it (SPGD-1510), and a comment-form run
369
381
  # separated from it by one blank/comment line (SPGD-1554).
370
- 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}/
371
390
 
372
391
  # A blank (whitespace-only) line — one of the two interleaves the
373
392
  # separated pass reads. `\n` is whitespace, so a `text.lines` element
@@ -574,7 +593,7 @@ module SpecGuard
574
593
  # only ever a missed flag: prose can turn the exemption ON, never a
575
594
  # real example OFF. The one-line example's own payload is never
576
595
  # consulted, so a payload can never exempt anything.
577
- EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*(?:it|specify)\b/
596
+ EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*#{EXAMPLE_CALL_SOURCE}/
578
597
 
579
598
  # @param text [String] source of one file
580
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.29"
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.29
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-10-01 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