specguard-ruby 0.3.1

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.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Back-compat alias. The version number itself lives in `SpecGuard::VERSION`
4
+ # (lib/specguard/version.rb) since the gem became `specguard-ruby`; this
5
+ # constant is kept because released `specguard-rspec` versions exposed it and
6
+ # a consumer reaching for it should not break on the rename. It is the same
7
+ # number, not a second one — `script/bump-version.sh` edits only the source.
8
+ require_relative "../version"
9
+
10
+ module SpecGuard
11
+ module RSpec
12
+ VERSION = SpecGuard::VERSION
13
+ end
14
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rspec/version"
4
+
5
+ module SpecGuard
6
+ 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
24
+
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
29
+
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 opt-in Go validator backend could not produce a verdict — the binary
37
+ # named by `SPECGUARD_VALIDATE_INTENT` is missing, will not execute, exited
38
+ # with a code that is not a verdict, or emitted something this cannot read
39
+ # as a report.
40
+ #
41
+ # Typed separately from {UsageError} because the two are different
42
+ # accusations ("you invoked me wrongly" vs "the tool I was told to use is
43
+ # broken"), and rescued *beside* it in {CLI#run} because the answer to both
44
+ # is the same and non-negotiable: exit 2, never 1. See {ValidatorBackend}.
45
+ class ValidatorError < Error; end
46
+
47
+ # The vendored canonical OpenTestIntent schema, copied byte-for-byte from
48
+ # open-test-intent so this gem has **no cross-repo runtime dependency**.
49
+ # It ships packaged (it lives under `lib/`, which the gemspec includes);
50
+ # the spec fixtures deliberately do not, so nothing here may be
51
+ # load-bearing at runtime.
52
+ #
53
+ # Since the SPGD-867 cutover nothing VALIDATES against this copy — every
54
+ # verdict comes from the `validate-intent` binary's own compiled-in
55
+ # schema. It survives because {ValidatorBackend}'s schema-contract check
56
+ # digests it at runtime and refuses a binary that would enforce different
57
+ # bytes: the two halves of the seam must keep meeting somewhere, and this
58
+ # file is where the gem's half lives.
59
+ SCHEMA_PATH = File.expand_path("rspec/schemas/open-test-intent.v1.json", __dir__).freeze
60
+ end
61
+ end
62
+
63
+ require_relative "rspec/finding"
64
+ require_relative "rspec/annotation_scanner"
65
+ require_relative "rspec/payload_normalizer"
66
+ require_relative "rspec/scanner"
67
+ require_relative "rspec/file_selector"
68
+ require_relative "rspec/linter"
69
+ require_relative "rspec/json_reporter"
70
+ require_relative "rspec/validator_backend"
71
+ require_relative "rspec/cli"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The version of the `specguard-ruby` gem — one number for every adapter the
4
+ # gem ships. It lived in `SpecGuard::RSpec::VERSION` while the gem itself was
5
+ # `specguard-rspec` and rspec was the only adapter; the rename to a
6
+ # language-scoped client (mirroring `specguard-ts` and `specguard-go`) made a
7
+ # framework-scoped version constant a lie, so the number moved here and the
8
+ # rspec module now points at it for back-compat.
9
+ module SpecGuard
10
+ VERSION = "0.3.1"
11
+ end
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # Bump the patch version, commit, push to main, and tag vX.Y.Z on what landed.
3
+ #
4
+ # Mirrors yatfa's script/bump-version.sh, adapted for a gem:
5
+ # - the version source is lib/specguard/version.rb (the gemspec reads
6
+ # SpecGuard::VERSION from it; lib/specguard/rspec/version.rb is a back-compat
7
+ # alias that follows it), not a VERSION file
8
+ # - release is tag-based like warden's: the GitHub Release and the pushed gem
9
+ # both correspond to vX.Y.Z. There is no `prod` deploy-mirror branch —
10
+ # nothing here is deployed to a cluster.
11
+ #
12
+ # Strategy carried over verbatim: must be on main with a clean tree, patch-only
13
+ # bump, commit "bump: X -> Y", pull --rebase + push with up to 3 retries.
14
+ #
15
+ # When run inside GitHub Actions (GITHUB_OUTPUT set), exports `version` and
16
+ # `tag` to the step's job outputs. Locally it just prints the new version.
17
+ set -e
18
+
19
+ VERSION_FILE="lib/specguard/version.rb"
20
+
21
+ # Ensure we're on the main branch
22
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
23
+ if [ "$CURRENT_BRANCH" != "main" ]; then
24
+ echo "Error: Not on main branch. Current branch: $CURRENT_BRANCH"
25
+ exit 1
26
+ fi
27
+
28
+ # Check that no TRACKED file has uncommitted changes. Untracked files are not
29
+ # a reason to refuse: this runs in CI right after the suite, where
30
+ # ruby/setup-ruby's bundler-cache rewrites Gemfile.lock (untracked here, see
31
+ # .gitignore) and the suite itself may leave artefacts behind. What must be
32
+ # clean is the content the bump is about to commit on top of.
33
+ if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
34
+ echo "Error: tracked files have uncommitted changes:"
35
+ git status --porcelain --untracked-files=no
36
+ exit 1
37
+ fi
38
+
39
+ if [ ! -f "$VERSION_FILE" ]; then
40
+ echo "Error: $VERSION_FILE not found"
41
+ exit 1
42
+ fi
43
+
44
+ CURRENT_VERSION=$(ruby -e 'require "./lib/specguard/rspec/version"; print SpecGuard::RSpec::VERSION')
45
+ echo "Current version: $CURRENT_VERSION"
46
+
47
+ # Bump patch version (0.2.0 -> 0.2.1)
48
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
49
+ NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
50
+ echo "New version: $NEW_VERSION"
51
+
52
+ # A published gem version is immutable — RubyGems refuses a re-push and yanking
53
+ # does not free the number. Refuse locally rather than discovering it after a
54
+ # build, and refuse a reused tag for the same reason the release does.
55
+ if git rev-parse -q --verify "refs/tags/v$NEW_VERSION" >/dev/null || \
56
+ git ls-remote --exit-code --tags origin "refs/tags/v$NEW_VERSION" >/dev/null 2>&1; then
57
+ echo "Error: tag v$NEW_VERSION already exists — the version file and the tags disagree"
58
+ exit 1
59
+ fi
60
+ if command -v curl >/dev/null 2>&1; then
61
+ if curl -fsS --max-time 10 "https://rubygems.org/api/v1/versions/specguard-rspec.json" 2>/dev/null \
62
+ | grep -q "\"number\":\"$NEW_VERSION\""; then
63
+ echo "Error: specguard-rspec $NEW_VERSION is already published on RubyGems"
64
+ exit 1
65
+ fi
66
+ fi
67
+
68
+ # Rewrite the VERSION constant in place. A targeted sed on the one line, so the
69
+ # surrounding file (frozen_string_literal, module nesting) is untouched.
70
+ sed -i.bak -E "s/(VERSION = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1$NEW_VERSION\2/" "$VERSION_FILE"
71
+ rm -f "$VERSION_FILE.bak"
72
+ WROTE=$(ruby -e 'load "./lib/specguard/rspec/version.rb"; print SpecGuard::RSpec::VERSION')
73
+ if [ "$WROTE" != "$NEW_VERSION" ]; then
74
+ echo "Error: $VERSION_FILE still reports $WROTE after the rewrite"
75
+ exit 1
76
+ fi
77
+
78
+ # Create commit
79
+ git add "$VERSION_FILE"
80
+ git commit -m "bump: $CURRENT_VERSION -> $NEW_VERSION"
81
+
82
+ # Pull with rebase to incorporate any commits that landed on main since checkout,
83
+ # then push the branch and the tag. Retry up to 3 times to handle concurrent pushes.
84
+ #
85
+ # The tag is created INSIDE the loop, after `git push origin main` succeeds, and
86
+ # never before the rebase. `git pull --rebase` replays the bump commit onto the
87
+ # advanced main and gives it a new SHA; tags do not follow a rebase, so a tag
88
+ # created beforehand would name an abandoned object that is not an ancestor of
89
+ # main — while the workflow builds the gem from the post-rebase tree. A published
90
+ # gem version is immutable, so that divergence would be permanent.
91
+ #
92
+ # `-f` makes a retry idempotent when an earlier attempt created the local tag but
93
+ # failed to push it. It cannot clobber a published tag: the tag-reuse guard above
94
+ # already refused that case before any work started.
95
+ MAX_RETRIES=3
96
+ RETRY_COUNT=0
97
+ PUSH_SUCCESS=false
98
+
99
+ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
100
+ if git pull --rebase origin main; then
101
+ # HEAD is now the rebased bump commit — the object the tag must name.
102
+ if git push origin main; then
103
+ git tag -f "v$NEW_VERSION" HEAD
104
+ if git push origin "v$NEW_VERSION"; then
105
+ PUSH_SUCCESS=true
106
+ break
107
+ fi
108
+ fi
109
+ fi
110
+ RETRY_COUNT=$((RETRY_COUNT + 1))
111
+ if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
112
+ echo "Push failed, retrying ($RETRY_COUNT/$MAX_RETRIES)..."
113
+ sleep 5
114
+ fi
115
+ done
116
+
117
+ if [ "$PUSH_SUCCESS" = false ]; then
118
+ echo "Error: Failed to push version bump after $MAX_RETRIES attempts"
119
+ exit 1
120
+ fi
121
+
122
+ echo "Version bumped to $NEW_VERSION and pushed to main with tag v$NEW_VERSION"
123
+
124
+ # Export to GitHub Actions job outputs when run in CI
125
+ if [ -n "$GITHUB_OUTPUT" ]; then
126
+ echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
127
+ echo "tag=v$NEW_VERSION" >> "$GITHUB_OUTPUT"
128
+ fi
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: specguard-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - specguard Agent
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.21'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.21'
27
+ description: SpecGuard's Ruby client — every Ruby test framework, one gem. Ships an
28
+ additive RSpec formatter and a Minitest reporter that post test-run telemetry to
29
+ SpecGuard's ingest endpoint, a CLI linter (specguard-lint) that validates OpenTestIntent
30
+ @intent annotations in *_spec.rb files, and a replayer (specguard-ingest) that re-delivers
31
+ a run saved when the endpoint could not be reached. Telemetry never blocks CI; the
32
+ linter blocks only on malformed annotations.
33
+ email:
34
+ - specguard-agent@noreply.yatfa.com
35
+ executables:
36
+ - specguard-lint
37
+ - specguard-ingest
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - ".github/workflows/ci.yml"
42
+ - ".github/workflows/release.yml"
43
+ - LICENSE
44
+ - README.md
45
+ - Rakefile
46
+ - assets/built-with-yatfa.png
47
+ - bin/specguard-ingest
48
+ - bin/specguard-lint
49
+ - lib/minitest/specguard_plugin.rb
50
+ - lib/specguard/minitest/reporter.rb
51
+ - 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
+ - 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
+ - lib/specguard/rspec/version.rb
69
+ - lib/specguard/version.rb
70
+ - script/bump-version.sh
71
+ homepage: https://github.com/yatfa-ai/specguard-ruby
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ homepage_uri: https://github.com/yatfa-ai/specguard-ruby
76
+ bug_tracker_uri: https://github.com/yatfa-ai/specguard-ruby/issues
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.2.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.4.19
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: RSpec formatter, Minitest reporter and @intent annotation linter for SpecGuard.
96
+ test_files: []