specguard-ruby 0.3.13 → 0.3.15

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: d6e99d2df23601a1d6118d833c56b67b868fe3e02598bbc67d097a0eb491d6a7
4
- data.tar.gz: ccef4a5ca06af0fa84e66091725bbdeb6b8ba5627a49d14e8ae7f51d19860c28
3
+ metadata.gz: 3475a2dc3666fdebc75d6c75f47589bd8d92fa22a1caf57f3a13e4d6082ce58a
4
+ data.tar.gz: f2c122923ced904d8ccc6c08c8ff93c013a40361a494523f3c4209e5605e0797
5
5
  SHA512:
6
- metadata.gz: 2bc085f8cfa17d999856abf4d161f2c6a1b1bbd58ee626c89b4a55de51c3dcf9e1b7ad8ee1a919dd9090cd3fa510eaec7ff4bf93a4bc7a51f9830faeec1c5b99
7
- data.tar.gz: c1aba6267102cbedf20135a3d7ad9010c3742e7c7405b88f34a374281b3b3f78ef349765638db34d01d8a4eaa9f393ce8a77def353de7a1bd79cdb08f1517343
6
+ metadata.gz: 80bb44b49402facf181703f2aee6faf94e3d42b55ab88182ad94056a43ed127ded3db724f8c68fc90a461dd79939cd29703277e3d4ea19783e94ee5bd4ab56f2
7
+ data.tar.gz: fb8deaf26aaea6e3d5e94fbfdf07ac66aa51a50f2292398e49accbefc34a4b1bf768733d026d1d2340a380a610c32af585aa95a8380677f12b0721b5fa2b3f3a
data/README.md CHANGED
@@ -51,9 +51,19 @@ above another comment-form `@intent:` line, so the one-line lookback never claim
51
51
 
52
52
  ```bash
53
53
  bundle exec specguard-lint --changed # CI mode: only files in the current diff
54
- bundle exec specguard-lint # one-off audit: every *_spec.rb
54
+ bundle exec specguard-lint # one-off audit: every *_spec.rb outside dependency/build directories
55
55
  ```
56
56
 
57
+ The default walk is fenced out of dependency and build directories — `node_modules`,
58
+ `vendor`, `tmp`, `log`, `dist`, `.test-build`, `coverage`, `.git`, matched as whole
59
+ directory names, so `spec/vendor_helpers/` is still yours. A bundled Rails tree
60
+ (`bundle install --deployment` → `vendor/bundle/`) ships thousands of third-party gem
61
+ specs; the walk neither reports on them nor fails over their annotations, and the
62
+ zero-annotation coverage note counts only files you wrote — the same material
63
+ `--changed` keeps out via `.gitignore`. When the fence removed files, the
64
+ `checked N spec files` line says how many (`skipping M in dependency or build
65
+ directories`); a file named explicitly on the command line is always checked.
66
+
57
67
  `--changed` is the CI selection mode: it diffs against the **merge base with
58
68
  the default branch** (`origin/HEAD`, then `origin/main`/`origin/master`, then
59
69
  their local names) — never a bare working-tree-vs-index `git diff`, which is
@@ -262,21 +262,43 @@ module SpecGuard
262
262
  # of record; the json renderer printing the SAME bytes is what makes
263
263
  # "the machine reads the provenance the human does" a property of the
264
264
  # code rather than a promise two copies could drift apart on.
265
+ #
266
+ # The `:all` count names its fence: `FileSelector.select_all` refuses
267
+ # dependency/build directories (`file_selector.rb`
268
+ # `SKIPPED_DIRECTORIES`), and a fence that actually removed files says
269
+ # how many — the narrowing must never be silent. The clause is
270
+ # count-gated exactly like the untracked one, so a run whose fence
271
+ # removed nothing prints byte-identically to before the fence existed.
265
272
  def selection_line(selection)
266
273
  untracked = selection.mode == :changed ? selection.stats&.untracked.to_i : 0
274
+ skipped = selection.mode == :all ? selection.skipped : 0
267
275
  "specguard-lint: checked #{selection.count} spec file#{'s' unless selection.count == 1}" \
268
276
  "#{" changed since #{selection.base}" if selection.mode == :changed}" \
269
277
  "#{" under #{Dir.pwd}" if selection.mode == :all}" \
270
- "#{" including #{untracked} untracked" if untracked.positive?}"
278
+ "#{" including #{untracked} untracked" if untracked.positive?}" \
279
+ "#{" skipping #{skipped} in dependency or build directories" if skipped.positive?}"
271
280
  end
272
281
 
273
282
  # `:explicit` is absent by construction — an explicit Selection is only
274
283
  # built from a non-empty file list, so it can never be empty.
275
284
  def empty_reason(selection)
276
- case selection.mode
277
- when :changed then changed_empty_reason(selection)
278
- else "no *_spec.rb or *_test.rb file found under #{Dir.pwd}"
279
- end
285
+ return changed_empty_reason(selection) if selection.mode == :changed
286
+ # An `:all` selection the fence emptied must not blame the tree for
287
+ # holding no spec files the files existed and the fence removed
288
+ # them, and "no file found" is the confidently wrong reason this
289
+ # ladder exists to prevent.
290
+ return all_fenced_reason(selection) if selection.mode == :all && selection.skipped.positive?
291
+
292
+ "no *_spec.rb or *_test.rb file found under #{Dir.pwd}"
293
+ end
294
+
295
+ # The fence arm of the `:all` empty reason: every matching file the walk
296
+ # found was inside a {SpecGuard::RSpec::FileSelector::SKIPPED_DIRECTORIES}
297
+ # directory, so the silence is the fence's, not the tree's.
298
+ def all_fenced_reason(selection)
299
+ n = selection.skipped
300
+ "#{n} *_spec.rb or *_test.rb file#{'s' unless n == 1} found, " \
301
+ "all in dependency or build directories"
280
302
  end
281
303
 
282
304
  # Names the filter that actually emptied the selection. Saying "nothing in
@@ -16,6 +16,14 @@ module SpecGuard
16
16
  # diff names paths, not directories, and a changed test file is a test
17
17
  # file wherever the author put it.
18
18
  #
19
+ # The default walk also carries a fixed directory fence
20
+ # ({SKIPPED_DIRECTORIES}): dependency, build-output, scratch and VCS
21
+ # directories are never selected, so a bundled tree's `vendor/bundle/`
22
+ # gem specs are neither reported on nor failed over — third-party code
23
+ # the user did not write. `--changed` draws the same boundary from
24
+ # `.gitignore` (`--exclude-standard`) for free; the walk, which must keep
25
+ # working outside a repository, carries it as a name list instead.
26
+ #
19
27
  # == Why `--changed` is not `git diff --name-only`
20
28
  #
21
29
  # The Client Gem spec words `--changed` as "files in the current diff (via
@@ -122,6 +130,44 @@ module SpecGuard
122
130
  # no competitor convention to fence against.
123
131
  DEFAULT_GLOB = ["**/*_spec.rb", "test/**/*_test.rb"].freeze
124
132
 
133
+ # A named-file default-walk fence, ported from this product's own
134
+ # TypeScript answer (`specguard-ts` `src/lint/discover.ts`
135
+ # `SKIPPED_DIRECTORIES`, widened by the Ruby ecosystem's members). Every
136
+ # member is dependency, build-output, scratch or VCS material — code the
137
+ # user did not write and cannot edit, whose specs must never be selected:
138
+ #
139
+ # * `node_modules` — npm dependencies (the port's founding member).
140
+ # * `.git` — VCS internals; `Dir.glob` already skips hidden
141
+ # directories, so this is belt-and-braces, named so the fence reads
142
+ # complete beside its TypeScript twin.
143
+ # * `dist` — build output.
144
+ # * `.test-build` — this project family's own build/test scratch.
145
+ # * `coverage` — SimpleCov's default report directory.
146
+ # * `vendor` — a bundled Rails tree (`bundle install --deployment`)
147
+ # puts every gem under `vendor/bundle/`, each shipping its own
148
+ # specs; the measured failure this fence exists for.
149
+ # * `tmp` — Rails' scratch directory (caches, pids, sockets).
150
+ # * `log` — Rails' log directory.
151
+ #
152
+ # `--changed` gets this boundary for free — `--exclude-standard` keeps
153
+ # `.gitignore`d paths (scratch directories, vendored code, build output)
154
+ # out of its untracked leg. The walk has no git to ask, because it must
155
+ # keep working outside a repository exactly where `--changed` correctly
156
+ # refuses, so it carries the boundary as a name list instead. The list
157
+ # is matched against whole directory SEGMENTS of the root-relative path
158
+ # (see {skipped_directory?}), never as a substring — `spec/vendor_helpers/`
159
+ # is project code — and never against the absolute path.
160
+ SKIPPED_DIRECTORIES = %w[
161
+ node_modules
162
+ .git
163
+ dist
164
+ .test-build
165
+ coverage
166
+ vendor
167
+ tmp
168
+ log
169
+ ].freeze
170
+
125
171
  # The `--changed` counterparts of {DEFAULT_GLOB}: git hands back paths,
126
172
  # so the filter is a match over the whole path rather than a glob walk,
127
173
  # and the suffix alone decides. Deliberately not directory-scoped — a
@@ -146,8 +192,19 @@ module SpecGuard
146
192
  end
147
193
 
148
194
  # What a selection produced, plus enough context to report it honestly.
149
- Selection = Data.define(:files, :mode, :base, :note, :stats) do
150
- def initialize(files:, mode:, base: nil, note: nil, stats: nil)
195
+ #
196
+ # `skipped` is the fence count for the `:all` mode ({SKIPPED_DIRECTORIES}),
197
+ # defaulted to 0 so every other construction site is untouched. It
198
+ # deliberately rides `Selection` rather than a new `Stats` member:
199
+ # `Stats` is documented as the `--changed` empty-reason ladder ("why an
200
+ # empty `--changed` selection is empty") and the fence is neither
201
+ # `--changed`-bound nor an emptiness explanation — it is report context
202
+ # for a selection that may be perfectly full. `Selection` is what this
203
+ # file already uses to carry per-mode context (`base`, `note`, `stats`
204
+ # are all `--changed`-only and defaulted the same way), so the count
205
+ # follows that precedent instead of widening `Stats`'s contract.
206
+ Selection = Data.define(:files, :mode, :base, :note, :stats, :skipped) do
207
+ def initialize(files:, mode:, base: nil, note: nil, stats: nil, skipped: 0)
151
208
  super
152
209
  end
153
210
 
@@ -174,11 +231,33 @@ module SpecGuard
174
231
  end
175
232
 
176
233
  # Every test file under `root`, recursively, in either naming
177
- # convention. Hidden directories are not traversed (no
178
- # `File::FNM_DOTMATCH`), so `.git` and friends are skipped.
234
+ # convention, minus anything whose path runs through a
235
+ # {SKIPPED_DIRECTORIES} directory. Hidden directories are not traversed
236
+ # (no `File::FNM_DOTMATCH`), so `.git` and friends are skipped.
237
+ #
238
+ # The fence is decided on the path `Dir.glob(base: root)` yields —
239
+ # already relative to `root` — never on an absolute path: fixture roots
240
+ # (and the whole spec suite's) live under `Dir.mktmpdir`, i.e. inside a
241
+ # directory *named* `tmp`, and a fence reading absolute paths would
242
+ # drop every file in the tree. How many files the fence removed is
243
+ # carried on the `Selection` (`skipped`) so the CLI can disclose the
244
+ # narrowing instead of doing it silently.
179
245
  def select_all(root: Dir.pwd)
180
- files = Dir.glob(DEFAULT_GLOB, base: root).select { |f| File.file?(File.join(root, f)) }.sort
181
- Selection.new(files: files, mode: :all)
246
+ candidates = Dir.glob(DEFAULT_GLOB, base: root).select { |f| File.file?(File.join(root, f)) }.sort
247
+ kept, fenced = candidates.partition { |f| !skipped_directory?(f) }
248
+ Selection.new(files: kept, mode: :all, skipped: fenced.length)
249
+ end
250
+
251
+ # Whether a root-relative path runs through a {SKIPPED_DIRECTORIES}
252
+ # directory. Whole segments only — `File::SEPARATOR`-delimited — so a
253
+ # directory merely *named after* a fenced word (`spec/vendor_helpers/`)
254
+ # is not fenced, and the basename is excluded from the segment set: a
255
+ # path's last component is the file itself, so `spec/tmpfile_spec.rb`
256
+ # is selected whatever its name contains.
257
+ def skipped_directory?(path)
258
+ segments = path.split(File::SEPARATOR)
259
+ segments.pop
260
+ segments.any? { |segment| SKIPPED_DIRECTORIES.include?(segment) }
182
261
  end
183
262
 
184
263
  def select_changed(base: nil, root: Dir.pwd)
@@ -7,7 +7,7 @@
7
7
  # == Why this file is not on `require "specguard/rspec"`'s chain
8
8
  #
9
9
  # `rspec` is a **development** dependency of this gem, not a runtime one
10
- # (specguard-rspec.gemspec declares exactly one runtime dependency: `json`,
10
+ # (specguard-ruby.gemspec declares exactly one runtime dependency: `json`,
11
11
  # pinned; since SPGD-867 the validator arrives as a resolved binary rather
12
12
  # than as a gem). `bin/specguard-lint` loads `specguard/rspec`, and it has to
13
13
  # keep working for someone who installed the gem to lint annotations on a
@@ -250,7 +250,8 @@ module SpecGuard
250
250
  # ALWAYS a list of strings — `[]` where the line landed and where a
251
251
  # refusal's body said nothing this gem could read, never `null` and never
252
252
  # a bare string. {JSONReporter}'s `errors` makes the identical guarantee
253
- # for the identical reason: `report.go:23-26` is explicit that a consumer
253
+ # for the identical reason: `JSONFinding` (open-test-intent,
254
+ # `cmd/validate-intent/report.go`) is explicit that a consumer
254
255
  # must never have to branch on the type of the field that says why
255
256
  # something failed, and one list is what lets a refusal's per-spec errors,
256
257
  # a socket error and "this line is not a run" be read by one code path.
@@ -46,9 +46,10 @@ module SpecGuard
46
46
  # * `errors` is ALWAYS a list of strings — `reasons` when the schema
47
47
  # rejected the annotation, `[problem]` when discovery could not produce
48
48
  # one at all, `[]` when it passed. Never null, never a bare string:
49
- # `report.go:23-26` is explicit that a consumer must never branch on its
50
- # type, and this is the one place the gem's mutually-exclusive
51
- # `problem`/`reasons` pair is normalised into the port's single list.
49
+ # `JSONFinding` (open-test-intent, `cmd/validate-intent/report.go`) is
50
+ # explicit that a consumer must never branch on its type, and this is
51
+ # the one place the gem's mutually-exclusive `problem`/`reasons` pair
52
+ # is normalised into the port's single list.
52
53
  # * `line` is null exactly where the finding is not line-scoped, which is
53
54
  # {Linter::Result#line_scoped?} — the same rule `#location` uses to
54
55
  # print `file` rather than `file:0`. `:0` is not somewhere a reader can
@@ -71,7 +72,8 @@ module SpecGuard
71
72
  #
72
73
  # `ok` is likewise handed in, derived from the exit code the text path would
73
74
  # also have produced rather than recomputed from the findings, following
74
- # `report.go:84-89` for the same reason.
75
+ # `Emit` (open-test-intent, `cmd/validate-intent/report.go`) for the same
76
+ # reason.
75
77
  #
76
78
  # `summary.failed`, by contrast, is counted here — from the findings this
77
79
  # document actually emitted, so `failed` always equals the number of entries
@@ -93,7 +95,8 @@ module SpecGuard
93
95
  # and read the line; it is still exactly one line, on every run, on both
94
96
  # arms.
95
97
  module JSONReporter
96
- # The port's `jsonSchemaID` (`report.go:19`), and the basename of the
98
+ # The port's `jsonSchemaID` (open-test-intent,
99
+ # `cmd/validate-intent/report.go`), and the basename of the
97
100
  # gem's own vendored schema. Pinned to each other by a spec: the document
98
101
  # names the protocol it validated against, so it must not be able to name
99
102
  # one the gem does not carry.
@@ -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.13"
10
+ VERSION = "0.3.15"
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.13
4
+ version: 0.3.15
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-16 00:00:00.000000000 Z
11
+ date: 2026-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json