jobcompat 0.2.0 → 0.3.0

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: 89c7b2c39f3fbd8e9e683deecc9a5edf2021f6b3ba9b60350635519621b28048
4
- data.tar.gz: c6beb6202b88b1bfcc4536d83479c82005e45d920d555ec5f3645e626e5d2aa2
3
+ metadata.gz: 6b1df789562ba30ff3474a112bd44e602d68b833b7d34c92b936f8e508e20484
4
+ data.tar.gz: ba09c1c7f2ce1f22d44cf690559e395584be8492c5abbbad85d492281b6849cd
5
5
  SHA512:
6
- metadata.gz: c730b84dc08928632344fa3e539db97c7a723356b19ba3e89a2c3f9efc0c40979c4e9ab26bf9ff6ad92c1c8beefda83e519c57eca7cde7230c169d8f5e31df4c
7
- data.tar.gz: 0d79be4a2b98032937d8f7c926722ac6a57ff4f899d408a17ecfdd64bb702e823e624d367a2921b3b251b21dff80cb7bf1394f45c322f3f588f5987c6c86824b
6
+ metadata.gz: c4e50e2e13f1bc1d3a3e667b63a3865bd01260f8b9bd462db724c0aa44641fd555f191a3059548df2ca408e13a081d3daeb993e4a805a7b9617d79034a36643b
7
+ data.tar.gz: aff92fbf9b66f5320958497d79a07fbf0defe4b56f1da444fddf69340478f72677e2cad601c5e9951d4de5e76a27f71095ba741e2c0d437be42a8eec75f9413a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-10-05
4
+
5
+ - Recognize static Sidekiq compatibility aliases, including finite chains and supported namespaces, to preserve old serialized identities across worker renames.
6
+ - Normalize Class-object producers through aliases to the terminal Class name while preserving exact Client String identities.
7
+ - Report relevant unresolved, cyclic, conflicting, or unsupported alias bindings conservatively as JC007.
8
+ - Emit JSON schema 3 with effective serialized-identity worker results, alias metadata, and resolved_alias presence; direct worker summary counts and config schema 1 remain unchanged.
9
+
10
+ ## 0.2.1 - 2026-10-04
11
+
12
+ Canonical worker-name consistency:
13
+
14
+ - Accept canonical Unicode worker names, such as names with combining marks and qualified `Admin::ÁJob`, in targeted `.jobcompat.yml` suppressions. Matching stays exact and is not Unicode-normalized.
15
+ - Share one Prism-based canonical worker-name validator between Client String producer analysis and Config.
16
+ - Report malformed, non-constant, or invalid-byte `ignore[].worker` names as normal config errors (exit 2). Names that are not Ruby constants, such as `管理::輸出`, were previously accepted but could never match a finding.
17
+
3
18
  ## 0.2.0 - 2026-10-02
4
19
 
5
20
  Broader native Sidekiq producer coverage:
data/README.md CHANGED
@@ -54,7 +54,7 @@ ERROR JC001 ExportJob
54
54
  Risk: Jobs queued by the base revision may fail after deployment.
55
55
  ```
56
56
 
57
- The second direction appears only when HEAD also has a one-argument producer. Output includes source locations, suggested migration steps, resolved commit SHAs, and a summary. JSON output uses `schema_version: 2` and includes per-worker compatibility matrices.
57
+ The second direction appears only when HEAD also has a one-argument producer. Output includes source locations, suggested migration steps, resolved commit SHAs, and a summary. JSON output uses `schema_version: 3` and includes per-serialized-identity compatibility matrices and alias provenance.
58
58
 
59
59
  ## How it works
60
60
 
@@ -111,7 +111,7 @@ ignore:
111
111
  reason: "Enqueue starts after deployment completes"
112
112
  ```
113
113
 
114
- Each configured `include` or `exclude` list **replaces** its default list, so copy the defaults you still want. `--config PATH` selects another YAML file; a relative path is resolved from the invocation directory. Unknown keys and unsafe YAML aliases fail with exit 2. An ignore entry must have an exact rule, canonical worker name, and non-blank reason. Matched suppressions appear in text and JSON audit output.
114
+ Each configured `include` or `exclude` list **replaces** its default list, so copy the defaults you still want. `--config PATH` selects another YAML file; a relative path is resolved from the invocation directory. Unknown keys and unsafe YAML aliases fail with exit 2. An ignore entry must have an exact rule, canonical worker name, and non-blank reason. `ignore[].worker` uses the same canonical Ruby constant-name rules as exact Client String worker targets, including valid Unicode identifiers; matching is exact and not Unicode-normalized. Matched suppressions appear in text and JSON audit output.
115
115
 
116
116
  ## CI usage
117
117
 
@@ -140,9 +140,36 @@ jobcompat sees payload arities 1 and 2. A dynamic bulk row is retained as `dynam
140
140
 
141
141
  Positional `perform` parameters may be required, optional, rest, post-rest, or forwarding. Keyword parameters make the worker contract unknown in v0.2.
142
142
 
143
- ## Limitations and v0.2 non-goals
143
+ ### Worker rename and static compatibility aliases
144
144
 
145
- v0.2 checks **positional arity only**. It does not check value types, Hash internals, keyword compatibility, or JSON serialization. It supports native Sidekiq, not ActiveJob. Dynamic bulk collections are warning/unknown, Client payload variables are not followed, and custom wrappers are not analyzed. ActiveJob, arbitrary Client instances, Hash internal schemas, aliases, inheritance, concerns, arbitrary metaprogramming, feature flags, and general Ruby class/module or superclass conflict analysis remain outside scope.
145
+ jobcompat recognizes plain static constant aliases to directly recognized jobs, including finite chains and supported namespaces:
146
+
147
+ ```ruby
148
+ class MyNewJob
149
+ include Sidekiq::Job
150
+ def perform(id); end
151
+ end
152
+
153
+ MyOldJob = MyNewJob
154
+ ```
155
+
156
+ Old queued `"MyOldJob"` payloads can resolve through the alias to the new worker contract, so this rename does not produce JC004. Arity incompatibilities still produce the normal findings under the persisted name.
157
+
158
+ `MyOldJob.perform_async(1)` uses the aliased Class object: Sidekiq serializes its canonical name **MyNewJob**, not the source alias token. This also applies to scheduling, bulk, Setter calls, and Client constant payloads. During a rolling deployment, activating that producer can still cause JC005 MyNewJob if old processes cannot resolve the new identity. A compatibility alias alone does not make immediate new-name producer activation safe.
159
+
160
+ By contrast, an explicit Client String keeps its exact identity:
161
+
162
+ ```ruby
163
+ Sidekiq::Client.push("class" => "MyOldJob", "args" => [1])
164
+ ```
165
+
166
+ jobcompat models that producer as MyOldJob, then follows the alias on the consumer side. This describes identity behavior; Client Strings also have different job-option/default semantics.
167
+
168
+ Only unconditional plain constant assignments are trusted. Cycles, conflicting or repeated bindings, dynamic assignments, and conditional aliases warn when relevant. Excluded aliases can block an absence proof but are not trusted as compatibility paths. There is no const_set/autoload/const_missing support, conditional alias proof, runtime load-order proof, or heuristic rename detection. Recognition assumes successful application boot, just as direct worker recognition does.
169
+
170
+ ## Limitations and non-goals
171
+
172
+ jobcompat checks **positional arity only**. It does not check value types, Hash internals, keyword compatibility, or JSON serialization. It supports native Sidekiq, not ActiveJob. Dynamic bulk collections are warning/unknown, Client payload variables are not followed, and custom wrappers are not analyzed. ActiveJob, arbitrary Client instances, Hash internal schemas, general Ruby aliases beyond the static subset above, inheritance, concerns, arbitrary metaprogramming, feature flags, and general Ruby class/module or superclass conflict analysis remain outside scope.
146
173
 
147
174
  No repository producer callsite does **not** prove an empty queue: queued, scheduled, retried, historical, and external jobs may exist. JC004 requires a completed static absence proof across tracked `.rb` source. An excluded or unrecognized declaration warns. JC005 is a structural rolling-deploy error conditional on an old Sidekiq process being able to consume the new job's queue. Queue isolation and feature flags are not analyzed. Dynamic source may warn or be missed. Working-tree edits are ignored.
148
175
 
@@ -171,7 +198,7 @@ Possible future work includes SARIF output and additional evidence sources. Othe
171
198
 
172
199
  ## Contributing
173
200
 
174
- Run `bundle exec rake test` and `gem build jobcompat.gemspec` with Ruby 3.3 or newer. Tests create temporary Git repositories and need no Redis or Sidekiq server. The [v0.1 specification](docs/spec-v0.1.md) remains the baseline; [the normative v0.2 delta](docs/spec-v0.2.md) defines the added producer coverage.
201
+ Run `bundle exec rake test` and `gem build jobcompat.gemspec` with Ruby 3.3 or newer. Tests create temporary Git repositories and need no Redis or Sidekiq server. The [v0.1 specification](docs/spec-v0.1.md) remains the baseline; [the normative v0.2 delta](docs/spec-v0.2.md) defines the added producer coverage, [the v0.2.1 delta](docs/spec-v0.2.1.md) defines shared canonical worker-name validation, and [the v0.3 delta](docs/spec-v0.3.md) defines static compatibility aliases and serialized identity resolution.
175
202
 
176
203
  ## License
177
204
 
data/docs/architecture.md CHANGED
@@ -1,7 +1,13 @@
1
- # jobcompat v0.2 architecture
1
+ # jobcompat v0.3.0 architecture
2
2
 
3
- Status: implemented design for `0.2.0`
4
- Date: 2026-09-28
3
+ Status: implemented design for `0.3.0`
4
+ Date: 2026-10-05
5
+
6
+ This document describes the current v0.3.0 architecture, extending the
7
+ historical v0.1 and v0.2 design. References to those releases record inherited
8
+ design decisions and additions. The current JSON output schema is 3;
9
+ schema 2 describes historical v0.2 behavior. `docs/spec-v0.3.md` defines the
10
+ normative v0.3.0 contract.
5
11
 
6
12
  ## 1. Architecture principles
7
13
 
@@ -191,7 +197,7 @@ It receives source bytes and a revision/path context. It does not open files, in
191
197
 
192
198
  `ConstantName` is a small utility that flattens only `ConstantReadNode` and `ConstantPathNode`. It returns a value object with `segments`, `root_qualified`, and `static`; it does not resolve Ruby constants.
193
199
 
194
- Client String targets use one pure analysis helper shared by `push` and `push_bulk`. After safe UTF-8 conversion and encoding validation, Prism parses the value in isolation. Only a single `ConstantReadNode` or static `ConstantPathNode` is accepted, using the existing constant extractor. Root-qualified Strings are rejected, and the extracted canonical name must equal the complete original text, excluding whitespace, comments, and extra syntax. This preserves Ruby's Unicode identifiers without normalization or lexical prefixing. Expected encoding failures and invalid syntax become `unsupported_client_payload`; unexpected parser failures remain tool failures. No source is executed.
200
+ Client String targets use the internal `CanonicalWorkerName` validator shared by `push`, `push_bulk`, and `.jobcompat.yml` `ignore[].worker` validation (since v0.2.1). After safe UTF-8 conversion and encoding validation, Prism parses the value in isolation. Only a single `ConstantReadNode` or static `ConstantPathNode` chain is accepted. Root-qualified Strings are rejected, and the extracted canonical name must equal the complete original text, excluding whitespace, comments, and extra syntax. This preserves Ruby's Unicode identifiers without normalization or lexical prefixing. Expected encoding failures and invalid syntax become `unsupported_client_payload`; unexpected parser failures remain tool failures. No source is executed.
195
201
 
196
202
  `DefinedConstantIndex` records exact or plausible canonical class/module declarations and statically named constant bindings, source locations, and normal-scan membership. It is distinct from supported Sidekiq worker recognition. For a name missing from one revision's recognized workers, it returns `recognized_worker`, `defined_unrecognized`, `outside_scan_scope`, `absent`, or `unverified` using the formal proof in the spec. A present-but-unrecognized class or incomplete presence pass blocks JC004/JC005 ERROR.
197
203
 
@@ -214,7 +220,7 @@ The engine MUST be pure with respect to filesystem, Git, environment, clock, and
214
220
  Formatters receive a completed or failed result envelope. They MUST NOT recalculate compatibility or suppression. They render already matched suppression audit records without reconstructing omitted findings.
215
221
 
216
222
  - `Formatter::Text` renders concise human remediation.
217
- - `Formatter::Json` constructs schema-version-2 primitive Hash/Array data, preserving the v1 shape while extending producer `unknown_reason` values. It includes each finding's `revisions`, `directions`, and `unknown_reason` and each worker's base/head presence status, calls `JSON.pretty_generate`, and appends exactly one newline.
223
+ - `Formatter::Json` constructs schema-version-3 primitive Hash/Array data, retaining the historical v0.2 fields and adding alias metadata and `resolved_alias` presence. It includes each finding's `revisions`, `directions`, and `unknown_reason` and each worker's base/head presence status, calls `JSON.pretty_generate`, and appends exactly one newline.
218
224
 
219
225
  Both consume already sorted results.
220
226
 
@@ -364,12 +370,46 @@ UnresolvedEnqueueCall
364
370
  unknown_reason Symbol | nil
365
371
  ```
366
372
 
367
- The reason values map one-to-one to the frozen baseline strings in `docs/spec-v0.1.md` plus the schema-v2 producer additions in `docs/spec-v0.2.md`; visitors do not invent free-form reason text.
373
+ The reason values retain the frozen baseline strings in `docs/spec-v0.1.md` and the historical v0.2/schema-v2 producer additions in `docs/spec-v0.2.md`, with current alias additions defined in `docs/spec-v0.3.md`; visitors do not invent free-form reason text.
368
374
 
369
375
  Only after both revisions' worker names are known does the engine resolve these facts. This avoids ordering dependence between files and enables head calls to removed base workers.
370
376
 
371
377
  ## 7. Internal models
372
378
 
379
+ ### v0.3.0 serialized identity resolution
380
+
381
+ The existing Analyzer Prism pass extracts direct worker facts together with
382
+ selected-source ConstantBinding and AliasCandidate facts. AliasResolver
383
+ builds a deterministic per-revision graph using the same constant-path
384
+ and lexical namespace rules, with iterative cycle detection and strict
385
+ binding uniqueness. DefinedConstantIndex remains separate presence proof;
386
+ its excluded-file facts never become trusted alias edges.
387
+ Selected ambiguous paths retain separate leaf-name binding blockers; the
388
+ resolver checks them at alias edges and direct terminals without evaluating
389
+ dynamic receivers. Unrecognized terminal declarations remain diagnostic
390
+ proof locations.
391
+
392
+ RevisionSnapshot retains direct `workers` for summary counts and adds
393
+ `aliases`, effective consumer contracts by serialized identity, and presence
394
+ helpers. A resolved alias carries terminal contract and every edge's proof
395
+ locations; an unknown binding masks a conflicting direct consumer.
396
+ The snapshot also retains selected binding facts so alias-related producer
397
+ lookup stops at nearer non-worker class/module declarations.
398
+
399
+ Engine normalizes Class-object calls through their own revision's alias
400
+ graph to terminal identities, preserving known identity even with unknown
401
+ arity. Exact Client Strings retain their identity. Diagnostic attribution
402
+ is separate from known serialized producer identity, so an unknown alias
403
+ cannot drive JC005.
404
+ Class-object alias calls carry terminal declaration/include/perform proof.
405
+ Opposite revision names cannot override local bindings during alias lookup;
406
+ the existing non-alias cross-revision attribution is preserved.
407
+ The bounded comparison set includes historical BASE aliases and relevant
408
+ producer/binding identities; dormant HEAD aliases do
409
+ not automatically add warnings. Existing rules consume effective contracts.
410
+ Schema 3 exposes base_alias/head_alias and resolved_alias presence while
411
+ summary.workers still counts direct worker facts.
412
+
373
413
  Models SHOULD be immutable `Data.define` values on Ruby 3.3. Constructors validate invariants at the boundary; rule methods do not repeatedly revalidate them.
374
414
 
375
415
  ### 7.1 `SourceLocation`
@@ -0,0 +1,17 @@
1
+ # jobcompat v0.2.1 implementation plan
2
+
3
+ Status: implemented
4
+
5
+ ## Delivery sequence
6
+
7
+ 1. Extract the Prism-based Client String worker-name check into the internal `Jobcompat::CanonicalWorkerName.parse` (`lib/jobcompat/canonical_worker_name.rb`), which requires `prism` itself and depends on neither Analyzer nor Config.
8
+ 2. Migrate `Analyzer#client_worker` to the shared validator, keeping `exact_string` and `unsupported_client_payload` behavior unchanged.
9
+ 3. Migrate `Config.validate` `ignore[].worker` to the shared validator, removing the handwritten regex grammar and keeping the existing `ignore[N].worker is invalid` config error.
10
+ 4. Add a focused validator unit matrix (accepted names, rejected syntax, invalid bytes, no normalization, standalone loading) and a producer/Config agreement check; extend Config unit tests for Unicode acceptance, invalid bytes, duplicates, and exact matching.
11
+ 5. Add temporary-Git integration coverage for decomposed Unicode JC003 suppression with JSON and text audit, precomposed/decomposed exact-match guards, qualified Unicode names, non-canonical and invalid-byte config errors, and byte determinism.
12
+ 6. Raise the gem version to 0.2.1 (JSON schema 2 and config version 1 unchanged) and synchronize README, CHANGELOG, architecture, and the normative delta.
13
+ 7. Run the full suite on the supported Ruby matrix with Prism 1.9.0, syntax checks, gem build/contents/isolated install, CLI exit-code smoke scenarios, and diff self-review.
14
+
15
+ ## Scope boundaries
16
+
17
+ No rule, finding field, uncertainty reason, config key, dependency, or release operation is added. Constant AST resolution, `DefinedConstantIndex`, the engine, formatter, and Git access are unchanged.
@@ -0,0 +1,32 @@
1
+ # v0.3.0 implementation plan
2
+
3
+ Implement the normative delta in spec-v0.3.md in this order.
4
+
5
+ 1. Extend Analyzer's existing Prism pass with immutable AliasCandidate and
6
+ ConstantBinding facts. Propagate a narrow direct-statement context for
7
+ alias proof, while retaining baseline worker/producer discovery. Keep
8
+ DefinedConstantIndex responsible only for conservative presence.
9
+ 2. Build AliasResolver from selected binding inventory and direct workers.
10
+ Reuse one constant candidate helper. Iterate each chain with cycle
11
+ detection; reject duplicate/conflicting bindings and unsupported edges.
12
+ Retain deterministic chain, locations, and revision-free fingerprints.
13
+ 3. Extend RevisionSnapshot with aliases and effective consumer/presence
14
+ helpers. Preserve workers as direct facts and summary counts. Derive
15
+ alias contracts from terminal contracts with complete proof locations.
16
+ 4. Resolve producers using snapshot-local alias semantics. Keep identity
17
+ certainty separate from arity certainty and diagnostic attribution.
18
+ Preserve exact Client Strings and baseline non-alias attribution.
19
+ 5. Feed relevant serialized identities and effective consumers into Engine.
20
+ Keep JC001-JC007 logic and precedence; add alias warnings/metadata and
21
+ update identity wording. Emit schema 3 on completed and failed CLI output.
22
+ 6. Add focused AST/graph/producer unit tests and temporary Git rule,
23
+ deployment, JSON, determinism, Unicode, suppression, and security tests.
24
+ Update only intentional version/schema expectations in existing tests.
25
+ 7. Update README, architecture, changelog, version and lockfile. Run all
26
+ tests in available Ruby with UTF-8 locale, build and inspect the gem,
27
+ install into a temporary isolated destination, run CLI exit scenarios,
28
+ inspect the full diff, and remove only the generated package.
29
+
30
+ Mandala tracks specification, aliases, producers, compatibility, validation.
31
+ Only evidence-backed completed facets are marked done. No release operation
32
+ is part of this plan.
@@ -0,0 +1,76 @@
1
+ # jobcompat v0.2.1 release checklist
2
+
3
+ This is preparation for a maintenance update to the already published `jobcompat` gem. Checked boxes record completed preparation checks; unchecked boxes are final release or post-publication gates, not claims of completion. No v0.2.1 tag or publication is authorized by this document.
4
+
5
+ ## Release identity
6
+
7
+ - Repository: `cottondesu/jobcompat`, branch `main`.
8
+ - Gem version: `0.2.1`; intended annotated tag: `v0.2.1`.
9
+ - Intended release date: `2026-10-04`. If execution is delayed, reconcile the CHANGELOG date with the actual intended date before tagging; never backdate publication.
10
+ - JSON schema: `2`; `.jobcompat.yml` configuration schema: `1`.
11
+ - Implementation commit: `fd6d138f229228be8801de9a7ae040cab21c9923` (`Fix Unicode worker suppression validation`), on top of the v0.2.0 release commit `378203731bcc0e4a06a1a3550fbd5605d9760f2e`.
12
+ - Implementation history note: the implementation was first pushed as `0397dc5b7b8eb4acaa975d8d3b628aa4f87aed8a`. At the maintainer's request, before any tag or publication, it was replaced once on `main` by `fd6d138` to correct the commit author and committer and remove a co-author trailer. Both commits have the identical tree `18602325cc75951aee1d078c92c4c7813443f570`, so the reviewed product bytes are unchanged. `0397dc5` is no longer on `main` and must not be tagged.
13
+ - Release candidate commit: to be determined after the `Prepare v0.2.1 release` commit. Record its exact SHA and CI run in the final release execution record; do not select a moving `main` ref without verification.
14
+
15
+ ## Reviewed implementation
16
+
17
+ - [x] Fresh independent review of the complete v0.2.1 diff: verdict SHIP, no BLOCKER, no MAJOR.
18
+ - [x] Independent validation: 140 tests, 1298 assertions, 0 failures, 0 errors, 0 skips with Prism 1.9.0 under a UTF-8 locale, on Ruby 3.3.6 (reviewer environment), Ruby 3.4.8 (conda-forge build), and Ruby 4.0.6 (conda-forge build). Official GitHub CI below is the release authority.
19
+ - [x] Reviewer MINOR, test-only: under a POSIX / US-ASCII locale, the Unicode suppression text assertion in `test/integration/check_command_test.rb` raises `Encoding::CompatibilityError` in the test itself. Production output is byte-identical between UTF-8 and POSIX locales; no production locale regression was observed. Non-release-blocking; deferred past v0.2.1 and intentionally left unchanged in this release. Canonical local validation uses a UTF-8 locale.
20
+
21
+ ## Automated validation
22
+
23
+ - [x] Implementation commit `fd6d138` passed [CI run 36992116824](https://github.com/cottondesu/jobcompat/actions/runs/36992116824): Ruby 3.3.12, 3.4.10, and 4.0.6 PASS; each ran 140 tests / 1298 assertions with 0 failures, errors, and skips and built `jobcompat-0.2.1.gem`. (Superseded commit `0397dc5` had passed [CI run 36990271965](https://github.com/cottondesu/jobcompat/actions/runs/36990271965) with the same tree.)
24
+ - [x] Candidate local `LANG=C.UTF-8 bundle exec rake test` on Ruby 3.3.6 / Prism 1.9.0: 140 tests, 1298 assertions, zero failures, errors, and skips.
25
+ - [x] Candidate `git diff --check` and release-only diff review pass; only `CHANGELOG.md` and the two v0.2.1 release documents change, and product bytes match the implementation commit.
26
+ - [x] Build the candidate Gem and inspect its entries, including `lib/jobcompat/canonical_worker_name.rb`, the v0.2.1 spec and implementation plan, and both v0.2.1 release documents. No local tooling state, credentials, caches, scratch files, patches, review files, or nested Gems are included.
27
+ - [x] Install the candidate Gem into a fresh isolated destination under a UTF-8 locale; `jobcompat --version` reports `jobcompat 0.2.1`; clean 0, compatibility error 1, warning-only 0, invalid config 2, invalid ref 2, and Unicode suppression 0 pass.
28
+ - [x] Release-note examples checked with the installed candidate and Prism 1.9.0: a `ÁJob` (`A` + `U+0301` + `Job`) suppression is valid, `管理::輸出` is a `config_error` with exit 2, and `Job管理::E輸出` is valid.
29
+ - [ ] Record the pushed release-preparation SHA and its CI run; require Ruby 3.3, 3.4, and 4.0 success, including the Gem build step, on that exact SHA before tagging. The implementation run above does not substitute for candidate CI.
30
+
31
+ ## Compatibility note
32
+
33
+ - [x] The v0.2.1 release notes state prominently that v0.2.0 could silently accept `ignore[].worker` names that are not Ruby constant names, that such entries could never match a finding under the canonical worker-name semantics, and that v0.2.1 rejects them as `config_error` with exit 2.
34
+ - [x] The note explains why `管理::輸出` is rejected (not a Ruby constant path) and shows that CJK constant paths such as `Job管理::E輸出` remain accepted.
35
+ - [x] The v0.2.0 limitation about combining-mark suppressions is not repeated, because v0.2.1 fixes it.
36
+
37
+ ## Publication prerequisites
38
+
39
+ - [x] On 2026-10-02, public RubyGems listed `0.1.0` and `0.2.0` and no `0.2.1`.
40
+ - [x] On 2026-10-02, local and remote tags were `v0.1.0` and `v0.2.0` only, and GitHub Releases contained `v0.1.0` and `v0.2.0` and no `v0.2.1`.
41
+ - [ ] Confirm GitHub environment `release` still exists, requires reviewer `cottondesu`, and permits tag pattern `v*`. The preparation tooling could not read environment settings, so this is a manual gate. Do not weaken these protections.
42
+ - [ ] Confirm the current RubyGems Trusted Publisher for `jobcompat` in the maintainer UI: owner `cottondesu`, repository `jobcompat`, workflow file `release.yml`, environment `release`. Public package metadata and earlier publications are not proof of the current publisher settings. Do not introduce a long-lived RubyGems API key. See [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/).
43
+ - [x] Release workflow retains the tag/version and public metadata gates. Gem homepage, source, changelog, and issue URLs refer to `cottondesu/jobcompat`.
44
+ - [x] Review the v0.2.1 release notes against the normative scope and inspect the built candidate package.
45
+ - [x] On 2026-10-04, current-state recheck: public RubyGems listed `0.2.0` and `0.1.0` and no `0.2.1` (versions API 404); remote tags were `v0.1.0` and `v0.2.0` only, with no local `v0.2.1` tag; GitHub Releases were `v0.2.0` and `v0.1.0`. Repeat this check immediately before release execution.
46
+ - [ ] Immediately before release execution, recheck that `0.2.1` is absent on RubyGems and `v0.2.1` is absent locally, remotely, and in GitHub Releases. Stop if any already exists; never overwrite it.
47
+
48
+ ## Tag gate
49
+
50
+ **Pushing `v0.2.1` triggers the Release workflow and may publish the Gem automatically. Treat tag push as a publication action.**
51
+
52
+ - [ ] Complete every pre-tag validation and publication prerequisite above, including the manual environment and Trusted Publisher checks and exact candidate CI.
53
+ - [ ] Obtain separate final release execution approval. Release preparation alone does not authorize tagging or publication.
54
+ - [ ] Confirm the exact release candidate SHA, product version, intended release date, and unchanged v0.1.0 and v0.2.0 release identities.
55
+ - [ ] Under that separate approval, create an annotated `v0.2.1` tag on the verified SHA and push only that tag. Do not force or push unrelated tags.
56
+
57
+ ## Release workflow verification
58
+
59
+ The unchanged `.github/workflows/release.yml` triggers on pushes matching `v*`. Its Ruby 3.3 / 3.4 / 4.0 tests precede the `publish` job through `needs: test`. Publication uses Ruby 4.0, environment `release`, GitHub OIDC `id-token: write`, the tag/version and public metadata checks, pinned `rubygems/configure-rubygems-credentials`, and `gem push`.
60
+
61
+ - [ ] Verify the triggered Release run refers to the approved tag and exact candidate SHA.
62
+ - [ ] Confirm all three test jobs pass and the publication environment approval is satisfied.
63
+ - [ ] Confirm `GITHUB_REF_NAME == "v#{Jobcompat::VERSION}"`, public metadata checks, publication-job tests, build, credentials setup, and push all succeed.
64
+ - [ ] Confirm RubyGems lists `jobcompat` version `0.2.1`; do not infer publication solely from a tag or a partial workflow result.
65
+
66
+ ## After publication
67
+
68
+ - [ ] Check RubyGems version, ownership, Ruby/dependency requirements, public URLs, and package contents.
69
+ - [ ] Install `jobcompat` 0.2.1 from RubyGems in a clean environment; verify version/help and representative CLI checks.
70
+ - [ ] Under the separate release approval, create or verify the GitHub Release for `v0.2.1` using the reviewed release notes and correct tag.
71
+ - [ ] Record the release workflow URL and publication evidence; monitor issue/security reports.
72
+ - [ ] Track the deferred test-only POSIX locale MINOR as a follow-up after v0.2.1; do not alter v0.2.1 bytes after publication.
73
+
74
+ ## Rollback
75
+
76
+ Do not overwrite or reuse version `0.2.1` after publication, move the published tag, or alter v0.1.0 or v0.2.0. Fix forward in a later patch version. If necessary, use the official [RubyGems yanking procedure](https://guides.rubygems.org/removing-a-published-gem/) and communicate the affected version; yanking does not remove installed copies.
@@ -0,0 +1,109 @@
1
+ # jobcompat v0.3.0 release checklist
2
+
3
+ This document prepares jobcompat v0.3.0 for release. Checked boxes record completed preparation evidence. Unchecked boxes are final tag, publication, GitHub Release, or post-publication gates. This preparation does not authorize creating or pushing `v0.3.0`.
4
+
5
+ **Pushing `v0.3.0` triggers the Release workflow and may publish the gem to RubyGems. Treat tag push as a publication action.**
6
+
7
+ ## Release identity
8
+
9
+ - Repository: `cottondesu/jobcompat`, branch `main`.
10
+ - Gem and CLI version: `0.3.0`; intended annotated tag: `v0.3.0`.
11
+ - Intended release date: `2026-10-05`. If execution is delayed and publication is no longer intended for this date, update the release date consistently before committing or tagging; never backdate publication.
12
+ - JSON schema: `3`; `.jobcompat.yml` configuration schema: `1`.
13
+ - Ruby: `>= 3.3`; Prism: `>= 1.9, < 2`; no new runtime dependencies.
14
+ - Implementation commit: `642e2c4c838db78d4e7ae6dd9ff3b958b6336483` (`Add worker rename alias compatibility`), parent `6f6c2deecad10111d70455dd343e1f89a3af088c`.
15
+ - Implementation CI: run `37302967087`.
16
+ - Release candidate SHA: determined by this preparation commit; record the exact SHA and its exact CI run in the final preparation report. The release-preparation commit, not the implementation commit, must become the eventual tag target if its exact CI passes.
17
+
18
+ ## Reviewed implementation
19
+
20
+ - [x] Fresh independent review: SHIP; 0 BLOCKER and 0 MAJOR.
21
+ - [x] Final checked-in suite baseline: 189 tests, 1,942 assertions, 0 failures, 0 errors, 0 skips.
22
+ - [x] Reviewer documentation MINOR resolved before the implementation commit.
23
+ - [x] Analysis does not execute source, boot Rails or Sidekiq, or connect to Redis.
24
+
25
+ ## Implementation CI evidence
26
+
27
+ Implementation commit `642e2c4c838db78d4e7ae6dd9ff3b958b6336483` passed GitHub Actions run `37302967087`:
28
+
29
+ - [x] Ruby 3.3 tests and gem build succeeded.
30
+ - [x] Ruby 3.4 tests and gem build succeeded.
31
+ - [x] Ruby 4.0 tests and gem build succeeded.
32
+
33
+ This implementation CI does not replace the required official CI on the release-preparation commit.
34
+
35
+ ## Release-only local validation
36
+
37
+ - [x] `LANG=C.UTF-8 bundle exec rake test` under Ruby 3.4.8 / Prism 1.9.0: 189 tests, 1,942 assertions, 0 failures, 0 errors, 0 skips.
38
+ - [x] `git diff --check` passes.
39
+ - [x] `gem build jobcompat.gemspec` succeeds; package inspected and generated `.gem` removed after inspection.
40
+ - [x] Both v0.3.0 release documents are packaged under the existing `docs/*.md` gemspec policy; package contains no generated or local tooling files.
41
+ - [x] Installed the candidate gem into a fresh isolated destination; `jobcompat --version` prints `jobcompat 0.3.0`.
42
+ - [x] The full suite and alias integration scenarios verify safe alias 0, JC001 1, JC005 1, warning-only alias uncertainty 0, invalid config 2, and invalid ref 2.
43
+ - [x] Alias integration scenarios verify safe alias resolution, Class-object producer identity, exact Client String identity, and schema 3 fields; product tests were not changed.
44
+
45
+ ## Publication prerequisites
46
+
47
+ - [ ] Manually confirm GitHub environment `release` exists, its required reviewer/protection is intact, and its tag deployment rule remains acceptable. Do not weaken the environment.
48
+ - [ ] Manually confirm the RubyGems Trusted Publisher for `jobcompat` matches owner `cottondesu`, repository `jobcompat`, workflow `release.yml`, and environment `release`. Do not introduce a long-lived RubyGems API key.
49
+
50
+ ## Public-state precheck (2026-10-05)
51
+
52
+ - [x] RubyGems lists `0.2.1`, `0.2.0`, and `0.1.0`; `0.3.0` is absent.
53
+ - [x] The local `v0.3.0` tag is absent; remote tags include `v0.2.1` and no `v0.3.0`.
54
+ - [x] GitHub Releases include `v0.2.1`, `v0.2.0`, and `v0.1.0`; `v0.3.0` is absent.
55
+ - [x] Historical `v0.2.1` annotated tag object is `b9ca1f7a3d0818ee23beff48907cb3d6a2e0eb1c`, peeled commit is `6f6c2deecad10111d70455dd343e1f89a3af088c`, and the GitHub Release and RubyGems version are present.
56
+ - [ ] Immediately before final release execution, repeat all v0.3.0 absence checks. Stop if any already exists; never overwrite it.
57
+
58
+ ## Release workflow audit
59
+
60
+ The inspected `.github/workflows/release.yml` is unchanged. It triggers on tag pushes matching `v*`; the Ruby 3.3 / 3.4 / 4.0 test matrix gates `publish` through `needs: test`. Publication runs on Ruby 4.0 in environment `release`, grants GitHub OIDC `id-token: write`, checks tag/version and public metadata, configures RubyGems credentials with `rubygems/configure-rubygems-credentials`, then runs `gem push`.
61
+
62
+ - [x] Workflow trigger, test matrix, publish dependency, Ruby version, environment, OIDC permission, tag/version gate, metadata gate, and publication command audited.
63
+ - [ ] Confirm the GitHub release environment and RubyGems Trusted Publisher manually before the final tag action.
64
+
65
+ ## Release-note examples and content
66
+
67
+ - [x] Release notes were compared with `docs/spec-v0.3.md`, `README.md`, implementation, and tests for alias resolution, Class-object normalization, exact Client Strings, JC004/JC005, JC007 reasons, schema 3 fields, and config schema 1.
68
+ - [x] The alias integration scenarios and full local suite passed against the unchanged candidate implementation.
69
+
70
+ ## Tag gate
71
+
72
+ **Pushing `v0.3.0` triggers the Release workflow and may publish `jobcompat 0.3.0` automatically. Treat tag push as a publication action.** Before any separately approved final release execution:
73
+
74
+ - [ ] Release-preparation commit is fixed and its exact SHA identified.
75
+ - [ ] Exact release-preparation SHA has green official CI, including Ruby 3.3, Ruby 3.4, and Ruby 4.0.
76
+ - [ ] GitHub `release` environment gate has been manually verified.
77
+ - [ ] RubyGems Trusted Publisher settings have been manually verified.
78
+ - [ ] RubyGems `0.3.0`, local and remote `v0.3.0` tags, and GitHub Release `v0.3.0` are still absent.
79
+ - [ ] Separate final release execution approval has been obtained.
80
+
81
+ ## Future tag requirements
82
+
83
+ Documented for the separately approved release-execution phase; do not perform during preparation:
84
+
85
+ - Create an annotated `v0.3.0` tag pointing exactly to the verified release-preparation commit, not `642e2c4c838db78d4e7ae6dd9ff3b958b6336483`.
86
+ - Never create a lightweight tag, force a tag, or move an existing tag.
87
+
88
+ ## Release workflow verification
89
+
90
+ - [ ] Release workflow triggered by `v0.3.0`.
91
+ - [ ] Workflow tag resolves to the approved release-candidate SHA.
92
+ - [ ] Ruby 3.3, Ruby 3.4, and Ruby 4.0 test jobs succeed.
93
+ - [ ] Publish job starts only after tests; environment approval succeeds.
94
+ - [ ] Tag/version and metadata gates succeed.
95
+ - [ ] Publish-job tests and gem build succeed.
96
+ - [ ] OIDC credentials setup and gem push succeed.
97
+ - [ ] RubyGems publicly lists version `0.3.0`; do not infer publication solely from workflow status.
98
+
99
+ ## Post-publication
100
+
101
+ - [ ] Verify RubyGems version `0.3.0`, Ruby `>= 3.3`, Prism `>= 1.9, < 2`, metadata URLs, and package contents.
102
+ - [ ] Install from public RubyGems in a fresh environment and verify `jobcompat --version`.
103
+ - [ ] Run representative CLI checks against the installed public gem.
104
+ - [ ] Create or verify GitHub Release `v0.3.0` using the reviewed release notes and confirm it points to the correct tag.
105
+ - [ ] Record the release workflow URL.
106
+
107
+ ## Rollback
108
+
109
+ Do not overwrite or reuse `0.3.0`, move `v0.3.0`, or rewrite existing release history after publication. Fix forward with a later version. If a published gem must be removed, follow the official [RubyGems yank procedure](https://guides.rubygems.org/removing-a-published-gem/); yanking does not remove copies already installed.
@@ -0,0 +1,61 @@
1
+ # jobcompat v0.2.1 release notes
2
+
3
+ jobcompat v0.2.1 is a maintenance release. It makes `.jobcompat.yml` suppressions accept exactly the canonical worker names that producer analysis can report, so a worker that jobcompat can find is also a worker you can suppress. It adds no producer API, compatibility rule, or configuration key.
4
+
5
+ ## Highlights
6
+
7
+ - `ignore[].worker` accepts canonical Unicode worker names, including names with combining marks such as `ÁJob` and qualified names such as `Admin::ÁJob`.
8
+ - Client String worker targets and `ignore[].worker` now use one shared Prism-based canonical worker-name check, so the two can no longer disagree.
9
+ - Worker names keep their exact codepoints. jobcompat performs no Unicode normalization.
10
+ - Malformed, non-constant, and invalid-byte `ignore[].worker` values are reported as configuration errors (exit 2).
11
+
12
+ ## Unicode suppression consistency
13
+
14
+ In v0.2.0, producer analysis could report a finding for a worker whose name contains a combining mark, but `.jobcompat.yml` rejected a suppression for that same name with `ignore[0].worker is invalid`. v0.2.1 fixes this. For example, with a worker named `A` + `U+0301 COMBINING ACUTE ACCENT` + `Job` (displayed as `ÁJob`):
15
+
16
+ ```yaml
17
+ version: 1
18
+
19
+ ignore:
20
+ - rule: JC003
21
+ worker: ÁJob
22
+ reason: intentional migration
23
+ ```
24
+
25
+ suppresses the matching JC003 finding, exits 0, and keeps the usual suppression audit record (rule, exact worker, reason, finding count) in text and JSON output.
26
+
27
+ Matching remains exact. jobcompat applies no NFC, NFD, NFKC, or NFKD normalization and no case folding. `ÁJob` written as the single codepoint `U+00C1` followed by `Job` and `ÁJob` written as `A` + `U+0301` + `Job` look the same but are different worker identities: a suppression for one does not suppress a finding for the other. Write the suppression with the same codepoints as the worker name in your source.
28
+
29
+ ## Configuration compatibility note
30
+
31
+ **An existing v0.2.0 configuration can now fail with exit 2.**
32
+
33
+ v0.2.0's configuration validator accepted some `ignore[].worker` strings that Ruby does not treat as constant names. Producer analysis can never report such a name, so those entries could never match a jobcompat finding and were silent no-op suppressions. v0.2.1 rejects them as configuration errors (`ignore[N].worker is invalid`, JSON `status: "failed"`, exit 2) instead of ignoring them.
34
+
35
+ For example, `管理::輸出` is rejected because neither segment starts with an uppercase letter, so Ruby parses it as method calls rather than a constant path. This does not mean CJK worker names are unsupported: `Job管理::E輸出` is a valid Ruby constant path and remains accepted, and analysis and suppression treat it like any other worker name.
36
+
37
+ ## Contracts unchanged
38
+
39
+ - JC001–JC007 meanings, severities, and precedence are unchanged.
40
+ - JSON output remains `schema_version: 2`.
41
+ - `.jobcompat.yml` remains `version: 1`.
42
+ - Exit codes are unchanged: 0 for no unsuppressed compatibility errors, 1 for compatibility errors, and 2 for tool, config, Git, parser, or usage failures.
43
+ - Ruby `>= 3.3` and Prism `>= 1.9, < 2` remain required.
44
+
45
+ No JSON or configuration schema migration is required.
46
+
47
+ ## Known limitations
48
+
49
+ ActiveJob, keyword compatibility, payload value types, Hash internal schemas, live Redis inspection, wrappers, aliases, general dataflow, and runtime metaprogramming remain outside scope. An undiscovered producer does not prove that a queue is empty. Analysis does not execute application code. See the [normative v0.2.1 delta](spec-v0.2.1.md) for the full canonical worker-name rules.
50
+
51
+ ## Upgrade notes
52
+
53
+ If your `ignore[].worker` entries are ordinary Ruby constant names, such as `ExportJob` or `Admin::ExportJob`, no configuration change is needed.
54
+
55
+ If an `ignore[].worker` entry is a string that is not a canonical Ruby constant name, v0.2.1 reports a configuration error and exits 2. That entry never suppressed anything in v0.2.0. Remove it, or correct it to the exact worker name that jobcompat reports in its findings.
56
+
57
+ ## Installation
58
+
59
+ After publication, install this version with `gem install jobcompat --version 0.2.1`. Bundler users can specify `gem "jobcompat", "~> 0.2.1", require: false`.
60
+
61
+ Fetch the intended comparison ref, then run `jobcompat check --base origin/main` or `bundle exec jobcompat check --base origin/main` in the application repository.
@@ -0,0 +1,115 @@
1
+ # jobcompat v0.3.0 release notes
2
+
3
+ jobcompat v0.3.0 adds static worker-rename compatibility analysis for native Sidekiq. Supported aliases can keep old serialized worker names resolvable during rolling deployments. The release also models the difference between Sidekiq producers that pass a Class object and Client producers that pass an exact String identity.
4
+
5
+ ## Highlights
6
+
7
+ - Recognize supported static compatibility aliases such as `OldJob = NewJob`.
8
+ - Resolve finite alias chains and supported namespaces conservatively.
9
+ - Avoid a false JC004 when a retained old serialized identity still resolves through a safe alias.
10
+ - Normalize Class-object producer identities through aliases to their terminal Class names.
11
+ - Preserve exact `Sidekiq::Client` String identities.
12
+ - Report relevant alias uncertainty as JC007 instead of assuming compatibility.
13
+ - Upgrade JSON output to schema version 3 and include alias provenance.
14
+
15
+ ## Safe worker rename compatibility aliases
16
+
17
+ For a rename, keep a static alias while old payloads may still be retained:
18
+
19
+ ```ruby
20
+ class NewExportJob
21
+ include Sidekiq::Job
22
+
23
+ def perform(id)
24
+ end
25
+ end
26
+
27
+ OldExportJob = NewExportJob
28
+ ```
29
+
30
+ Queued, retried, or scheduled payloads already serialized as `"OldExportJob"` can resolve through this alias to the `NewExportJob` contract. A safe alias can therefore prevent JC004 for `OldExportJob` while those payloads may still run.
31
+
32
+ An alias does not hide a real argument incompatibility. If the terminal worker no longer accepts an old payload's positional arity, jobcompat can still report JC001 or JC006 under the existing rules.
33
+
34
+ Supported aliases are unconditional plain constant assignments, including supported namespace forms. Finite chains are resolved; for example, `LegacyExportJob = PreviousExportJob` and `PreviousExportJob = ExportJob` make `ExportJob` the effective consumer. This is a documented static subset, not general Ruby constant or alias resolution.
35
+
36
+ ## Serialized identity: Class objects vs exact Strings
37
+
38
+ Ruby aliases refer to the same Class object. Given:
39
+
40
+ ```ruby
41
+ OldExportJob = NewExportJob
42
+ OldExportJob.perform_async(1)
43
+ ```
44
+
45
+ the source token is `OldExportJob`, but the value passed to Sidekiq is the `NewExportJob` Class object. Sidekiq serializes that object's canonical name, so jobcompat models the persisted identity as `NewExportJob`. During a rolling deployment, activating this producer can still produce JC005 for `NewExportJob` if the old fleet does not recognize that name.
46
+
47
+ **Keeping `OldExportJob = NewExportJob` protects retained old `"OldExportJob"` payloads, but calling `OldExportJob.perform_async(...)` is not a mechanism for continuing to enqueue the old serialized class name.**
48
+
49
+ An explicit Client String has different identity behavior:
50
+
51
+ ```ruby
52
+ Sidekiq::Client.push(
53
+ "class" => "OldExportJob",
54
+ "args" => [1]
55
+ )
56
+ ```
57
+
58
+ Here the serialized identity remains exactly `OldExportJob`; jobcompat models that String as-is and can resolve it through the consumer-side alias. Client String pushes are described to explain identity modeling, not as the default rename strategy; their job-option and default semantics differ.
59
+
60
+ ## Conservative alias uncertainty
61
+
62
+ jobcompat does not assume compatibility when Ruby behavior is ambiguous. Relevant uncertainty is reported as JC007 with one of these reasons:
63
+
64
+ - `alias_target_unresolved`: the target cannot be proven to end at a recognized worker.
65
+ - `alias_cycle`: aliases refer back to each other.
66
+ - `alias_binding_conflict`: the same constant has conflicting definitions or writes.
67
+ - `unsupported_alias_assignment`: the assignment is conditional, dynamic, operator-based, or otherwise outside the supported syntax.
68
+
69
+ Unknown does not mean compatible. jobcompat does not execute application source to resolve these cases. It supports plain static assignments such as `OldJob = NewJob`, supported qualified paths, and supported namespace bodies. It does not resolve `const_set`, `autoload`, `const_missing`, conditional aliases, dynamic right-hand-side evaluation, runtime load order, general Ruby constant evaluation, or heuristic renames.
70
+
71
+ ## JSON schema 3
72
+
73
+ **v0.3.0 changes JSON `schema_version` from 2 to 3.** `workers[]` now represents compared serialized worker identities and exposes alias provenance. Each worker result includes `base_alias` and `head_alias`; the presence value `resolved_alias` identifies a successfully resolved alias. `summary.workers.base` and `.head` continue to count directly recognized Sidekiq workers, not aliases.
74
+
75
+ The `.jobcompat.yml` configuration schema remains `version: 1`. Do not change the configuration version to 3.
76
+
77
+ ## Compatibility contracts
78
+
79
+ No new JC rule was added. JC001–JC007 IDs, severities, JC001/JC003 precedence, the three rolling-deployment directions, exit codes, and suppression identity remain unchanged. The configuration schema remains version 1, Ruby `>= 3.3` remains required, and Prism `>= 1.9, < 2` remains the supported range.
80
+
81
+ ## Upgrade notes
82
+
83
+ **Consumers that parse jobcompat JSON must update to accept `schema_version: 3` before upgrading to v0.3.0.** Do not assume that a schema 2 consumer can read schema 3 unless that consumer explicitly supports new schema versions and fields.
84
+
85
+ No `.jobcompat.yml` version change is needed. Keep a compatibility alias for as long as old serialized identities can remain in queues, retries, schedules, or other retained payloads. Plan producer activation separately: calling an aliased Class object's Sidekiq API serializes the terminal Class name.
86
+
87
+ ## Known limitations
88
+
89
+ Analysis covers positional arity for native Sidekiq workers only. ActiveJob and keyword compatibility are unsupported. jobcompat does not inspect Hash internal schemas, payload value types, live Redis contents, general dataflow, arbitrary Client instances, wrapper inference, runtime load order, or general aliasing and metaprogramming. It does not execute application code.
90
+
91
+ No repository producer callsite does not prove an empty queue. Queued, scheduled, retried, historical, and external jobs may still exist; static analysis does not establish runtime queue contents.
92
+
93
+ ## Installation
94
+
95
+ ```bash
96
+ gem install jobcompat --version 0.3.0
97
+ ```
98
+
99
+ Bundler users can add:
100
+
101
+ ```ruby
102
+ gem "jobcompat", "~> 0.3.0", require: false
103
+ ```
104
+
105
+ Then fetch the comparison ref and run:
106
+
107
+ ```bash
108
+ jobcompat check --base origin/main
109
+ ```
110
+
111
+ or:
112
+
113
+ ```bash
114
+ bundle exec jobcompat check --base origin/main
115
+ ```